diff options
Diffstat (limited to 'spec/ruby/core/fiber')
| -rw-r--r-- | spec/ruby/core/fiber/alive_spec.rb | 44 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/blocking_spec.rb | 73 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/current_spec.rb | 50 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/fixtures/classes.rb | 16 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/fixtures/scheduler.rb | 35 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/inspect_spec.rb | 35 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/kill_spec.rb | 88 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/new_spec.rb | 8 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/raise_spec.rb | 186 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/resume_spec.rb | 30 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/scheduler_spec.rb | 8 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/set_scheduler_spec.rb | 8 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/shared/blocking.rb | 41 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/shared/resume.rb | 58 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/shared/scheduler.rb | 51 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/storage_spec.rb | 177 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/transfer_spec.rb | 84 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/yield_spec.rb | 4 |
18 files changed, 901 insertions, 95 deletions
diff --git a/spec/ruby/core/fiber/alive_spec.rb b/spec/ruby/core/fiber/alive_spec.rb new file mode 100644 index 0000000000..6fb1229d95 --- /dev/null +++ b/spec/ruby/core/fiber/alive_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' + +describe "Fiber#alive?" do + it "returns true for a Fiber that hasn't had #resume called" do + fiber = Fiber.new { true } + fiber.alive?.should == true + end + + # FIXME: Better description? + it "returns true for a Fiber that's yielded to the caller" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + fiber.alive?.should == true + end + + it "returns true when called from its Fiber" do + fiber = Fiber.new { fiber.alive?.should == true } + fiber.resume + end + + it "doesn't invoke the block associated with the Fiber" do + offthehook = mock('do not call') + offthehook.should_not_receive(:ring) + fiber = Fiber.new { offthehook.ring } + fiber.alive? + end + + it "returns false for a Fiber that's dead" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.resume }.should.raise(FiberError) + fiber.alive?.should == false + end + + it "always returns false for a dead Fiber" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.resume }.should.raise(FiberError) + fiber.alive?.should == false + -> { fiber.resume }.should.raise(FiberError) + fiber.alive?.should == false + fiber.alive?.should == false + end +end diff --git a/spec/ruby/core/fiber/blocking_spec.rb b/spec/ruby/core/fiber/blocking_spec.rb new file mode 100644 index 0000000000..d5caf81fbe --- /dev/null +++ b/spec/ruby/core/fiber/blocking_spec.rb @@ -0,0 +1,73 @@ +require_relative '../../spec_helper' +require_relative 'shared/blocking' + +describe "Fiber.blocking?" do + it_behaves_like :non_blocking_fiber, -> { Fiber.blocking? } + + context "when fiber is blocking" do + context "root Fiber of the main thread" do + it "returns 1 for blocking: true" do + fiber = Fiber.new(blocking: true) { Fiber.blocking? } + blocking = fiber.resume + + blocking.should == 1 + end + end + + context "root Fiber of a new thread" do + it "returns 1 for blocking: true" do + thread = Thread.new do + fiber = Fiber.new(blocking: true) { Fiber.blocking? } + blocking = fiber.resume + + blocking.should == 1 + end + + thread.join + end + end + end +end + +describe "Fiber#blocking?" do + it_behaves_like :non_blocking_fiber, -> { Fiber.current.blocking? } + + context "when fiber is blocking" do + context "root Fiber of the main thread" do + it "returns true for blocking: true" do + fiber = Fiber.new(blocking: true) { Fiber.current.blocking? } + blocking = fiber.resume + + blocking.should == true + end + end + + context "root Fiber of a new thread" do + it "returns true for blocking: true" do + thread = Thread.new do + fiber = Fiber.new(blocking: true) { Fiber.current.blocking? } + blocking = fiber.resume + + blocking.should == true + end + + thread.join + end + end + end +end + +describe "Fiber.blocking" do + context "when fiber is non-blocking" do + it "can become blocking" do + fiber = Fiber.new(blocking: false) do + Fiber.blocking do |f| + f.blocking? ? :blocking : :non_blocking + end + end + + blocking = fiber.resume + blocking.should == :blocking + end + end +end diff --git a/spec/ruby/core/fiber/current_spec.rb b/spec/ruby/core/fiber/current_spec.rb new file mode 100644 index 0000000000..cc5c9117b6 --- /dev/null +++ b/spec/ruby/core/fiber/current_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "Fiber.current" do + it "returns the root Fiber when called outside of a Fiber" do + root = Fiber.current + root.should.instance_of?(Fiber) + # We can always transfer to the root Fiber; it will never die + 5.times do + root.transfer.should == nil + root.alive?.should == true + end + end + + it "returns the current Fiber when called from a Fiber" do + fiber = Fiber.new do + this = Fiber.current + this.should.instance_of?(Fiber) + this.should == fiber + this.alive?.should == true + end + fiber.resume + end + + it "returns the current Fiber when called from a Fiber that transferred to another" do + states = [] + fiber = Fiber.new do + states << :fiber + this = Fiber.current + this.should.instance_of?(Fiber) + this.should == fiber + this.alive?.should == true + end + + fiber2 = Fiber.new do + states << :fiber2 + fiber.transfer + flunk + end + + fiber3 = Fiber.new do + states << :fiber3 + fiber2.transfer + states << :fiber3_terminated + end + + fiber3.resume + + states.should == [:fiber3, :fiber2, :fiber, :fiber3_terminated] + end +end diff --git a/spec/ruby/core/fiber/fixtures/classes.rb b/spec/ruby/core/fiber/fixtures/classes.rb index c00facd6e1..6b0e0fbc42 100644 --- a/spec/ruby/core/fiber/fixtures/classes.rb +++ b/spec/ruby/core/fiber/fixtures/classes.rb @@ -1,10 +1,20 @@ module FiberSpecs class NewFiberToRaise - def self.raise(*args) - fiber = Fiber.new { Fiber.yield } + def self.raise(*args, **kwargs, &block) + fiber = Fiber.new do + if block_given? + block.call do + Fiber.yield + end + else + Fiber.yield + end + end + fiber.resume - fiber.raise(*args) + + fiber.raise(*args, **kwargs) end end diff --git a/spec/ruby/core/fiber/fixtures/scheduler.rb b/spec/ruby/core/fiber/fixtures/scheduler.rb new file mode 100644 index 0000000000..16bd2f6b44 --- /dev/null +++ b/spec/ruby/core/fiber/fixtures/scheduler.rb @@ -0,0 +1,35 @@ +module FiberSpecs + + class LoggingScheduler + attr_reader :events + def initialize + @events = [] + end + + def block(*args) + @events << { event: :block, fiber: Fiber.current, args: args } + Fiber.yield + end + + def io_wait(*args) + @events << { event: :io_wait, fiber: Fiber.current, args: args } + Fiber.yield + end + + def kernel_sleep(*args) + @events << { event: :kernel_sleep, fiber: Fiber.current, args: args } + Fiber.yield + end + + def unblock(*args) + @events << { event: :unblock, fiber: Fiber.current, args: args } + Fiber.yield + end + + def fiber_interrupt(*args) + @events << { event: :fiber_interrupt, fiber: Fiber.current, args: args } + Fiber.yield + end + end + +end diff --git a/spec/ruby/core/fiber/inspect_spec.rb b/spec/ruby/core/fiber/inspect_spec.rb new file mode 100644 index 0000000000..fcfef20716 --- /dev/null +++ b/spec/ruby/core/fiber/inspect_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Fiber#inspect" do + describe "status" do + it "is resumed for the root Fiber of a Thread" do + inspected = Thread.new { Fiber.current.inspect }.value + inspected.should =~ /\A#<Fiber:0x\h+ .*\(resumed\)>\z/ + end + + it "is created for a Fiber which did not run yet" do + inspected = Fiber.new {}.inspect + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(created\)>\z/ + end + + it "is resumed for a Fiber which was resumed" do + inspected = Fiber.new { Fiber.current.inspect }.resume + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(resumed\)>\z/ + end + + it "is resumed for a Fiber which was transferred" do + inspected = Fiber.new { Fiber.current.inspect }.transfer + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(resumed\)>\z/ + end + + it "is suspended for a Fiber which was resumed and yielded" do + inspected = Fiber.new { Fiber.yield }.tap(&:resume).inspect + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(suspended\)>\z/ + end + + it "is terminated for a Fiber which has terminated" do + inspected = Fiber.new {}.tap(&:resume).inspect + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(terminated\)>\z/ + end + end +end diff --git a/spec/ruby/core/fiber/kill_spec.rb b/spec/ruby/core/fiber/kill_spec.rb new file mode 100644 index 0000000000..abf23ff176 --- /dev/null +++ b/spec/ruby/core/fiber/kill_spec.rb @@ -0,0 +1,88 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +describe "Fiber#kill" do + it "kills a non-resumed fiber" do + fiber = Fiber.new{} + + fiber.alive?.should == true + + fiber.kill + fiber.alive?.should == false + end + + it "kills a resumed fiber" do + fiber = Fiber.new{while true; Fiber.yield; end} + fiber.resume + + fiber.alive?.should == true + + fiber.kill + fiber.alive?.should == false + end + + it "can kill itself" do + fiber = Fiber.new do + Fiber.current.kill + end + + fiber.alive?.should == true + + fiber.resume + fiber.alive?.should == false + end + + it "kills a resumed fiber from a child" do + parent = Fiber.new do + child = Fiber.new do + parent.kill + parent.alive?.should == true + end + + child.resume + end + + parent.resume + parent.alive?.should == false + end + + it "executes the ensure block" do + ensure_executed = false + + fiber = Fiber.new do + while true; Fiber.yield; end + ensure + ensure_executed = true + end + + fiber.resume + fiber.kill + ensure_executed.should == true + end + + it "does not execute rescue block" do + rescue_executed = false + + fiber = Fiber.new do + while true; Fiber.yield; end + rescue Exception + rescue_executed = true + end + + fiber.resume + fiber.kill + rescue_executed.should == false + end + + it "repeatedly kills a fiber" do + fiber = Fiber.new do + while true; Fiber.yield; end + ensure + while true; Fiber.yield; end + end + + fiber.kill + fiber.alive?.should == false + end +end diff --git a/spec/ruby/core/fiber/new_spec.rb b/spec/ruby/core/fiber/new_spec.rb index b43c1386be..d31167496d 100644 --- a/spec/ruby/core/fiber/new_spec.rb +++ b/spec/ruby/core/fiber/new_spec.rb @@ -4,7 +4,7 @@ describe "Fiber.new" do it "creates a fiber from the given block" do fiber = Fiber.new {} fiber.resume - fiber.should be_an_instance_of(Fiber) + fiber.should.instance_of?(Fiber) end it "creates a fiber from a subclass" do @@ -12,17 +12,17 @@ describe "Fiber.new" do end fiber = MyFiber.new {} fiber.resume - fiber.should be_an_instance_of(MyFiber) + fiber.should.instance_of?(MyFiber) end it "raises an ArgumentError if called without a block" do - -> { Fiber.new }.should raise_error(ArgumentError) + -> { Fiber.new }.should.raise(ArgumentError) end it "does not invoke the block" do invoked = false fiber = Fiber.new { invoked = true } - invoked.should be_false + invoked.should == false fiber.resume end diff --git a/spec/ruby/core/fiber/raise_spec.rb b/spec/ruby/core/fiber/raise_spec.rb index f56219e565..107e5bd4ce 100644 --- a/spec/ruby/core/fiber/raise_spec.rb +++ b/spec/ruby/core/fiber/raise_spec.rb @@ -2,100 +2,140 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' require_relative '../../shared/kernel/raise' -ruby_version_is "2.7" do - describe "Fiber#raise" do - it_behaves_like :kernel_raise, :raise, FiberSpecs::NewFiberToRaise +describe "Fiber#raise" do + it "is a public method" do + Fiber.public_instance_methods.should.include?(:raise) end - describe "Fiber#raise" do - it 'raises RuntimeError by default' do - -> { FiberSpecs::NewFiberToRaise.raise }.should raise_error(RuntimeError) - end + it_behaves_like :kernel_raise, :raise, FiberSpecs::NewFiberToRaise + it_behaves_like :kernel_raise_across_contexts, :raise, FiberSpecs::NewFiberToRaise + ruby_version_is "4.0" do + it_behaves_like :kernel_raise_with_cause, :raise, FiberSpecs::NewFiberToRaise + end - it "raises FiberError if Fiber is not born" do - fiber = Fiber.new { true } - -> { fiber.raise }.should raise_error(FiberError, "cannot raise exception on unborn fiber") - end + it 'raises RuntimeError by default' do + -> { FiberSpecs::NewFiberToRaise.raise }.should.raise(RuntimeError) + end - it "raises FiberError if Fiber is dead" do - fiber = Fiber.new { true } - fiber.resume - -> { fiber.raise }.should raise_error(FiberError, /dead fiber called|attempt to resume a terminated fiber/) - end + it "raises FiberError if Fiber is not born" do + fiber = Fiber.new { true } + -> { fiber.raise }.should.raise(FiberError, "cannot raise exception on unborn fiber") + end - it 'accepts error class' do - -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError }.should raise_error(FiberSpecs::CustomError) - end + it "raises FiberError if Fiber is dead" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.raise }.should.raise(FiberError, /dead fiber called|attempt to resume a terminated fiber/) + end - it 'accepts error message' do - -> { FiberSpecs::NewFiberToRaise.raise "error message" }.should raise_error(RuntimeError, "error message") - end + it 'accepts error class' do + -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError }.should.raise(FiberSpecs::CustomError) + end - it 'does not accept array of backtrace information only' do - -> { FiberSpecs::NewFiberToRaise.raise ['foo'] }.should raise_error(TypeError) - end + it 'accepts error message' do + -> { FiberSpecs::NewFiberToRaise.raise "error message" }.should.raise(RuntimeError, "error message") + end - it 'does not accept integer' do - -> { FiberSpecs::NewFiberToRaise.raise 100 }.should raise_error(TypeError) - end + it 'does not accept array of backtrace information only' do + -> { FiberSpecs::NewFiberToRaise.raise ['foo'] }.should.raise(TypeError) + end - it 'accepts error class with error message' do - -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error' }.should raise_error(FiberSpecs::CustomError, 'test error') - end + it 'does not accept integer' do + -> { FiberSpecs::NewFiberToRaise.raise 100 }.should.raise(TypeError) + end + + it 'accepts error class with error message' do + -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error' }.should.raise(FiberSpecs::CustomError, 'test error') + end + + it 'accepts error class with error message and backtrace information' do + -> { + FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error', ['foo', 'boo'] + }.should.raise(FiberSpecs::CustomError) { |e| + e.message.should == 'test error' + e.backtrace.should == ['foo', 'boo'] + } + end + + it 'does not accept only error message and backtrace information' do + -> { FiberSpecs::NewFiberToRaise.raise 'test error', ['foo', 'boo'] }.should.raise(TypeError) + end - it 'accepts error class with with error message and backtrace information' do + it "raises a FiberError if invoked from a different Thread" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + Thread.new do -> { - FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error', ['foo', 'boo'] - }.should raise_error(FiberSpecs::CustomError) { |e| - e.message.should == 'test error' - e.backtrace.should == ['foo', 'boo'] - } - end + fiber.raise + }.should.raise(FiberError, "fiber called across threads") + end.join + end - it 'does not accept only error message and backtrace information' do - -> { FiberSpecs::NewFiberToRaise.raise 'test error', ['foo', 'boo'] }.should raise_error(TypeError) - end + it "kills Fiber" do + fiber = Fiber.new { Fiber.yield :first; :second } + fiber.resume + -> { fiber.raise }.should.raise + -> { fiber.resume }.should.raise(FiberError, /dead fiber called|attempt to resume a terminated fiber/) + end - it "raises a FiberError if invoked from a different Thread" do - fiber = Fiber.new { Fiber.yield } - fiber.resume - Thread.new do - -> { - fiber.raise - }.should raise_error(FiberError, "fiber called across threads") - end.join + it "returns to calling fiber after raise" do + fiber_one = Fiber.new do + Fiber.yield :yield_one + :unreachable end - it "kills Fiber" do - fiber = Fiber.new { Fiber.yield :first; :second } - fiber.resume - -> { fiber.raise }.should raise_error - -> { fiber.resume }.should raise_error(FiberError, /dead fiber called|attempt to resume a terminated fiber/) + fiber_two = Fiber.new do + results = [] + results << fiber_one.resume + begin + fiber_one.raise + rescue + results << :rescued + end + results end + + fiber_two.resume.should == [:yield_one, :rescued] end -end + ruby_version_is "3.4" do + it "raises on the resumed fiber" do + root_fiber = Fiber.current + f1 = Fiber.new { root_fiber.transfer } + f2 = Fiber.new { f1.resume } + f2.transfer -ruby_version_is "2.7"..."3.0" do - describe "Fiber#raise" do - it "raises a FiberError if invoked on a transferring Fiber" do - require "fiber" - root = Fiber.current - fiber = Fiber.new { root.transfer } - fiber.transfer - -> { fiber.raise }.should raise_error(FiberError, "cannot resume transferred Fiber") + -> do + f2.raise(RuntimeError, "Expected error") + end.should.raise(RuntimeError, "Expected error") end - end -end -ruby_version_is "3.0" do - describe "Fiber#raise" do - it "transfers and raises on a transferring fiber" do - require "fiber" - root = Fiber.current - fiber = Fiber.new { root.transfer } - fiber.transfer - -> { fiber.raise "msg" }.should raise_error(RuntimeError, "msg") + it "raises on itself" do + -> do + Fiber.current.raise(RuntimeError, "Expected error") + end.should.raise(RuntimeError, "Expected error") end + + it "should raise on parent fiber" do + f2 = nil + f1 = Fiber.new do + # This is equivalent to Kernel#raise: + f2.raise(RuntimeError, "Expected error") + end + f2 = Fiber.new do + f1.resume + end + + -> do + f2.resume + end.should.raise(RuntimeError, "Expected error") + end + end + + it "transfers and raises on a transferring fiber" do + root = Fiber.current + fiber = Fiber.new { root.transfer } + fiber.transfer + -> { fiber.raise "msg" }.should.raise(RuntimeError, "msg") end end diff --git a/spec/ruby/core/fiber/resume_spec.rb b/spec/ruby/core/fiber/resume_spec.rb index 273bc866af..e183cc10d9 100644 --- a/spec/ruby/core/fiber/resume_spec.rb +++ b/spec/ruby/core/fiber/resume_spec.rb @@ -1,5 +1,5 @@ require_relative '../../spec_helper' -require_relative '../../shared/fiber/resume' +require_relative 'shared/resume' describe "Fiber#resume" do it_behaves_like :fiber_resume, :resume @@ -28,18 +28,9 @@ describe "Fiber#resume" do fiber.resume :second end - ruby_version_is '3.0' do - it "raises a FiberError if the Fiber tries to resume itself" do - fiber = Fiber.new { fiber.resume } - -> { fiber.resume }.should raise_error(FiberError, /current fiber/) - end - end - - ruby_version_is '' ... '3.0' do - it "raises a FiberError if the Fiber tries to resume itself" do - fiber = Fiber.new { fiber.resume } - -> { fiber.resume }.should raise_error(FiberError, /double resume/) - end + it "raises a FiberError if the Fiber tries to resume itself" do + fiber = Fiber.new { fiber.resume } + -> { fiber.resume }.should.raise(FiberError, /current fiber/) end it "returns control to the calling Fiber if called from one" do @@ -76,4 +67,17 @@ describe "Fiber#resume" do ruby_exe(code).should == "ensure executed\n" end + + it "can work with Fiber#transfer" do + fiber1 = Fiber.new { true } + fiber2 = Fiber.new { fiber1.transfer; Fiber.yield 10 ; Fiber.yield 20; raise } + fiber2.resume.should == 10 + fiber2.resume.should == 20 + end + + it "raises a FiberError if the Fiber attempts to resume a resuming fiber" do + root_fiber = Fiber.current + fiber1 = Fiber.new { root_fiber.resume } + -> { fiber1.resume }.should.raise(FiberError, /attempt to resume a resuming fiber/) + end end diff --git a/spec/ruby/core/fiber/scheduler_spec.rb b/spec/ruby/core/fiber/scheduler_spec.rb new file mode 100644 index 0000000000..15a03c1479 --- /dev/null +++ b/spec/ruby/core/fiber/scheduler_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/scheduler' + +require "fiber" + +describe "Fiber.scheduler" do + it_behaves_like :scheduler, :scheduler +end diff --git a/spec/ruby/core/fiber/set_scheduler_spec.rb b/spec/ruby/core/fiber/set_scheduler_spec.rb new file mode 100644 index 0000000000..82f6acbe86 --- /dev/null +++ b/spec/ruby/core/fiber/set_scheduler_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/scheduler' + +require "fiber" + +describe "Fiber.scheduler" do + it_behaves_like :scheduler, :set_scheduler +end diff --git a/spec/ruby/core/fiber/shared/blocking.rb b/spec/ruby/core/fiber/shared/blocking.rb new file mode 100644 index 0000000000..21707e1ea7 --- /dev/null +++ b/spec/ruby/core/fiber/shared/blocking.rb @@ -0,0 +1,41 @@ +describe :non_blocking_fiber, shared: true do + context "root Fiber of the main thread" do + it "returns false" do + fiber = Fiber.new { @method.call } + blocking = fiber.resume + + blocking.should == false + end + + it "returns false for blocking: false" do + fiber = Fiber.new(blocking: false) { @method.call } + blocking = fiber.resume + + blocking.should == false + end + end + + context "root Fiber of a new thread" do + it "returns false" do + thread = Thread.new do + fiber = Fiber.new { @method.call } + blocking = fiber.resume + + blocking.should == false + end + + thread.join + end + + it "returns false for blocking: false" do + thread = Thread.new do + fiber = Fiber.new(blocking: false) { @method.call } + blocking = fiber.resume + + blocking.should == false + end + + thread.join + end + end +end diff --git a/spec/ruby/core/fiber/shared/resume.rb b/spec/ruby/core/fiber/shared/resume.rb new file mode 100644 index 0000000000..ff4bb72c73 --- /dev/null +++ b/spec/ruby/core/fiber/shared/resume.rb @@ -0,0 +1,58 @@ +describe :fiber_resume, shared: true do + it "can be invoked from the root Fiber" do + fiber = Fiber.new { :fiber } + fiber.send(@method).should == :fiber + end + + it "raises a FiberError if invoked from a different Thread" do + fiber = Fiber.new { 42 } + Thread.new do + -> { + fiber.send(@method) + }.should.raise(FiberError) + end.join + + # Check the Fiber can still be used + fiber.send(@method).should == 42 + end + + it "passes control to the beginning of the block on first invocation" do + invoked = false + fiber = Fiber.new { invoked = true } + fiber.send(@method) + invoked.should == true + end + + it "returns the last value encountered on first invocation" do + fiber = Fiber.new { 1+1; true } + fiber.send(@method).should == true + end + + it "runs until the end of the block" do + obj = mock('obj') + obj.should_receive(:do).once + fiber = Fiber.new { 1 + 2; a = "glark"; obj.do } + fiber.send(@method) + end + + it "accepts any number of arguments" do + fiber = Fiber.new { |a| } + -> { fiber.send(@method, *(1..10).to_a) }.should_not.raise + end + + it "raises a FiberError if the Fiber is dead" do + fiber = Fiber.new { true } + fiber.send(@method) + -> { fiber.send(@method) }.should.raise(FiberError) + end + + it "raises a LocalJumpError if the block includes a return statement" do + fiber = Fiber.new { return; } + -> { fiber.send(@method) }.should.raise(LocalJumpError) + end + + it "raises a LocalJumpError if the block includes a break statement" do + fiber = Fiber.new { break; } + -> { fiber.send(@method) }.should.raise(LocalJumpError) + end +end diff --git a/spec/ruby/core/fiber/shared/scheduler.rb b/spec/ruby/core/fiber/shared/scheduler.rb new file mode 100644 index 0000000000..04bdded53a --- /dev/null +++ b/spec/ruby/core/fiber/shared/scheduler.rb @@ -0,0 +1,51 @@ +describe :scheduler, shared: true do + it "validates the scheduler for required methods" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + required_methods.each do |missing_method| + scheduler = Object.new + required_methods.difference([missing_method]).each do |method| + scheduler.define_singleton_method(method) {} + end + -> { + suppress_warning { Fiber.set_scheduler(scheduler) } + }.should.raise(ArgumentError, /Scheduler must implement ##{missing_method}/) + end + end + + it "can set and get the scheduler" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + suppress_warning { Fiber.set_scheduler(scheduler) } + Fiber.scheduler.should == scheduler + end + + it "returns the scheduler after setting it" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + result = suppress_warning { Fiber.set_scheduler(scheduler) } + result.should == scheduler + end + + it "can remove the scheduler" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + suppress_warning { Fiber.set_scheduler(scheduler) } + Fiber.set_scheduler(nil) + Fiber.scheduler.should == nil + end + + it "can assign a nil scheduler multiple times" do + Fiber.set_scheduler(nil) + Fiber.set_scheduler(nil) + Fiber.scheduler.should == nil + end +end diff --git a/spec/ruby/core/fiber/storage_spec.rb b/spec/ruby/core/fiber/storage_spec.rb new file mode 100644 index 0000000000..a3f6bf9cad --- /dev/null +++ b/spec/ruby/core/fiber/storage_spec.rb @@ -0,0 +1,177 @@ +require_relative '../../spec_helper' + +describe "Fiber.new(storage:)" do + it "creates a Fiber with the given storage" do + storage = {life: 42} + fiber = Fiber.new(storage: storage) { Fiber.current.storage } + fiber.resume.should == storage + end + + it "creates a fiber with lazily initialized storage" do + Fiber.new(storage: nil) { Fiber[:x] = 10; Fiber.current.storage }.resume.should == {x: 10} + end + + it "creates a fiber by inheriting the storage of the parent fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Fiber.new { Fiber.current.storage }.resume + end + fiber.resume.should == {life: 42} + end + + it "cannot create a fiber with non-hash storage" do + -> { Fiber.new(storage: 42) {} }.should.raise(TypeError) + end + + it "cannot create a fiber with a frozen hash as storage" do + -> { Fiber.new(storage: {life: 43}.freeze) {} }.should.raise(FrozenError) + end + + it "cannot create a fiber with a storage hash with non-symbol keys" do + -> { Fiber.new(storage: {life: 43, Object.new => 44}) {} }.should.raise(TypeError) + end +end + +describe "Fiber#storage" do + it "cannot be accessed from a different fiber" do + f = Fiber.new(storage: {life: 42}) { nil } + -> { + f.storage + }.should.raise(ArgumentError, /Fiber storage can only be accessed from the Fiber it belongs to/) + end +end + +describe "Fiber#storage=" do + it "can clear the storage of the fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Fiber.current.storage = nil + Fiber[:x] = 10 + Fiber.current.storage + end + fiber.resume.should == {x: 10} + end + + it "can set the storage of the fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Fiber.current.storage = {life: 43} + Fiber.current.storage + end + fiber.resume.should == {life: 43} + end + + it "can't set the storage of the fiber to non-hash" do + -> { Fiber.current.storage = 42 }.should.raise(TypeError) + end + + it "can't set the storage of the fiber to a frozen hash" do + -> { Fiber.current.storage = {life: 43}.freeze }.should.raise(FrozenError) + end + + it "can't set the storage of the fiber to a hash with non-symbol keys" do + -> { Fiber.current.storage = {life: 43, Object.new => 44} }.should.raise(TypeError) + end +end + +describe "Fiber.[]" do + it "returns the value of the given key in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:life] }.resume.should == 42 + end + + it "returns nil if the key is not present in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:death] }.resume.should == nil + end + + it "returns nil if the current fiber has no storage" do + Fiber.new { Fiber[:life] }.resume.should == nil + end + + it "can use dynamically defined keys" do + key = :"#{self.class.name}#.#{self.object_id}" + Fiber.new { Fiber[key] = 42; Fiber[key] }.resume.should == 42 + end + + it "can't use invalid keys" do + invalid_keys = [Object.new, 12] + invalid_keys.each do |key| + -> { Fiber[key] }.should.raise(TypeError) + end + end + + ruby_bug "#20978", ""..."3.4" do + it "can use keys as strings" do + key = Object.new + def key.to_str; "Foo"; end + Fiber.new { Fiber[key] = 42; Fiber["Foo"] }.resume.should == 42 + end + + it "converts a String key into a Symbol" do + Fiber.new { Fiber["key"] = 42; Fiber[:key] }.resume.should == 42 + Fiber.new { Fiber[:key] = 42; Fiber["key"] }.resume.should == 42 + end + + it "can use any object that responds to #to_str as a key" do + key = mock("key") + key.should_receive(:to_str).twice.and_return("key") + Fiber.new { Fiber[key] = 42; Fiber[key] }.resume.should == 42 + end + end + + it "does not call #to_sym on the key" do + key = mock("key") + key.should_not_receive(:to_sym) + -> { Fiber[key] }.should.raise(TypeError) + end + + it "can access the storage of the parent fiber" do + f = Fiber.new(storage: {life: 42}) do + Fiber.new { Fiber[:life] }.resume + end + f.resume.should == 42 + end + + it "can't access the storage of the fiber with non-symbol keys" do + -> { Fiber[Object.new] }.should.raise(TypeError) + end +end + +describe "Fiber.[]=" do + it "sets the value of the given key in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:life] = 43; Fiber[:life] }.resume.should == 43 + end + + it "sets the value of the given key in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:death] = 43; Fiber[:death] }.resume.should == 43 + end + + it "sets the value of the given key in the storage of the current fiber" do + Fiber.new { Fiber[:life] = 43; Fiber[:life] }.resume.should == 43 + end + + it "does not overwrite the storage of the parent fiber" do + f = Fiber.new(storage: {life: 42}) do + Fiber.yield Fiber.new { Fiber[:life] = 43; Fiber[:life] }.resume + Fiber[:life] + end + f.resume.should == 43 # Value of the inner fiber + f.resume.should == 42 # Value of the outer fiber + end + + it "can't access the storage of the fiber with non-symbol keys" do + -> { Fiber[Object.new] = 44 }.should.raise(TypeError) + end + + it "deletes the fiber storage key when assigning nil" do + Fiber.new(storage: {life: 42}) { + Fiber[:life] = nil + Fiber.current.storage + }.resume.should == {} + end +end + +describe "Thread.new" do + it "creates a thread with the storage of the current fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Thread.new { Fiber.current.storage }.value + end + fiber.resume.should == {life: 42} + end +end diff --git a/spec/ruby/core/fiber/transfer_spec.rb b/spec/ruby/core/fiber/transfer_spec.rb new file mode 100644 index 0000000000..d8737aeeb3 --- /dev/null +++ b/spec/ruby/core/fiber/transfer_spec.rb @@ -0,0 +1,84 @@ +require_relative '../../spec_helper' +require_relative 'shared/resume' + +describe "Fiber#transfer" do + it_behaves_like :fiber_resume, :transfer +end + +describe "Fiber#transfer" do + it "transfers control from one Fiber to another when called from a Fiber" do + fiber1 = Fiber.new { :fiber1 } + fiber2 = Fiber.new { fiber1.transfer; :fiber2 } + fiber2.resume.should == :fiber2 + end + + it "returns to the root Fiber when finished" do + f1 = Fiber.new { :fiber_1 } + f2 = Fiber.new { f1.transfer; :fiber_2 } + + f2.transfer.should == :fiber_1 + f2.transfer.should == :fiber_2 + end + + it "can be invoked from the same Fiber it transfers control to" do + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.transfer + states.should == [:start, :end] + + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.resume + states.should == [:start, :end] + end + + it "can not transfer control to a Fiber that has suspended by Fiber.yield" do + states = [] + fiber1 = Fiber.new { states << :fiber1 } + fiber2 = Fiber.new { states << :fiber2_start; Fiber.yield fiber1.transfer; states << :fiber2_end} + fiber2.resume.should == [:fiber2_start, :fiber1] + -> { fiber2.transfer }.should.raise(FiberError) + end + + it "raises a FiberError when transferring to a Fiber which resumes itself" do + fiber = Fiber.new { fiber.resume } + -> { fiber.transfer }.should.raise(FiberError) + end + + it "works if Fibers in different Threads each transfer to a Fiber in the same Thread" do + # This catches a bug where Fibers are running on a thread-pool + # and Fibers from a different Ruby Thread reuse the same native thread. + # Caching the Ruby Thread based on the native thread is not correct in that case, + # and the check for "fiber called across threads" in Fiber#transfer + # might be incorrect based on that. + 2.times do + Thread.new do + io_fiber = Fiber.new do |calling_fiber| + calling_fiber.transfer + end + io_fiber.transfer(Fiber.current) + value = Object.new + io_fiber.transfer(value).should.equal? value + end.join + end + end + + it "transfers control between a non-main thread's root fiber to a child fiber and back again" do + states = [] + thread = Thread.new do + f1 = Fiber.new do |f0| + states << 0 + value2 = f0.transfer(1) + states << value2 + 3 + end + + value1 = f1.transfer(Fiber.current) + states << value1 + value3 = f1.transfer(2) + states << value3 + end + thread.join + states.should == [0, 1, 2, 3] + end +end diff --git a/spec/ruby/core/fiber/yield_spec.rb b/spec/ruby/core/fiber/yield_spec.rb index b010912c87..12ec6ebcef 100644 --- a/spec/ruby/core/fiber/yield_spec.rb +++ b/spec/ruby/core/fiber/yield_spec.rb @@ -18,7 +18,7 @@ describe "Fiber.yield" do it "returns nil to the caller if given no arguments" do fiber = Fiber.new { true; Fiber.yield; true } - fiber.resume.should be_nil + fiber.resume.should == nil fiber.resume end @@ -44,6 +44,6 @@ describe "Fiber.yield" do end it "raises a FiberError if called from the root Fiber" do - ->{ Fiber.yield }.should raise_error(FiberError) + ->{ Fiber.yield }.should.raise(FiberError) end end |
