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 | 22 | ||||
| -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 | 90 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/new_spec.rb | 39 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/raise_spec.rb | 139 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/resume_spec.rb | 83 | ||||
| -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 | 181 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/transfer_spec.rb | 84 | ||||
| -rw-r--r-- | spec/ruby/core/fiber/yield_spec.rb | 49 |
18 files changed, 1090 insertions, 0 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..a1df582435 --- /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 be_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 be_true + end + + it "returns true when called from its Fiber" do + fiber = Fiber.new { fiber.alive?.should be_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_error(FiberError) + fiber.alive?.should be_false + end + + it "always returns false for a dead Fiber" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.resume }.should raise_error(FiberError) + fiber.alive?.should be_false + -> { fiber.resume }.should raise_error(FiberError) + fiber.alive?.should be_false + fiber.alive?.should be_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..b93df77a89 --- /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 be_an_instance_of(Fiber) + # We can always transfer to the root Fiber; it will never die + 5.times do + root.transfer.should be_nil + root.alive?.should be_true + end + end + + it "returns the current Fiber when called from a Fiber" do + fiber = Fiber.new do + this = Fiber.current + this.should be_an_instance_of(Fiber) + this.should == fiber + this.alive?.should be_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 be_an_instance_of(Fiber) + this.should == fiber + this.alive?.should be_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 new file mode 100644 index 0000000000..6b0e0fbc42 --- /dev/null +++ b/spec/ruby/core/fiber/fixtures/classes.rb @@ -0,0 +1,22 @@ +module FiberSpecs + + class NewFiberToRaise + 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, **kwargs) + end + end + + class CustomError < StandardError; 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..2f4c499280 --- /dev/null +++ b/spec/ruby/core/fiber/kill_spec.rb @@ -0,0 +1,90 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +ruby_version_is "3.3" do + 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 +end diff --git a/spec/ruby/core/fiber/new_spec.rb b/spec/ruby/core/fiber/new_spec.rb new file mode 100644 index 0000000000..b43c1386be --- /dev/null +++ b/spec/ruby/core/fiber/new_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +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) + end + + it "creates a fiber from a subclass" do + class MyFiber < Fiber + end + fiber = MyFiber.new {} + fiber.resume + fiber.should be_an_instance_of(MyFiber) + end + + it "raises an ArgumentError if called without a block" do + -> { Fiber.new }.should raise_error(ArgumentError) + end + + it "does not invoke the block" do + invoked = false + fiber = Fiber.new { invoked = true } + invoked.should be_false + fiber.resume + end + + it "closes over lexical environments" do + o = Object.new + def o.f + a = 1 + f = Fiber.new { a = 2 } + f.resume + a + end + o.f.should == 2 + end +end diff --git a/spec/ruby/core/fiber/raise_spec.rb b/spec/ruby/core/fiber/raise_spec.rb new file mode 100644 index 0000000000..896f760290 --- /dev/null +++ b/spec/ruby/core/fiber/raise_spec.rb @@ -0,0 +1,139 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +describe "Fiber#raise" do + it_behaves_like :kernel_raise, :raise, FiberSpecs::NewFiberToRaise + it_behaves_like :kernel_raise_across_contexts, :raise, FiberSpecs::NewFiberToRaise +end + +describe "Fiber#raise" do + it 'raises RuntimeError by default' do + -> { FiberSpecs::NewFiberToRaise.raise }.should raise_error(RuntimeError) + 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 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 'accepts error class' do + -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError }.should raise_error(FiberSpecs::CustomError) + end + + it 'accepts error message' do + -> { FiberSpecs::NewFiberToRaise.raise "error message" }.should raise_error(RuntimeError, "error message") + end + + it 'does not accept array of backtrace information only' do + -> { FiberSpecs::NewFiberToRaise.raise ['foo'] }.should raise_error(TypeError) + end + + it 'does not accept integer' do + -> { FiberSpecs::NewFiberToRaise.raise 100 }.should raise_error(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 'accepts error class with error message and backtrace information' 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 + + it 'does not accept only error message and backtrace information' do + -> { FiberSpecs::NewFiberToRaise.raise 'test error', ['foo', 'boo'] }.should raise_error(TypeError) + 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 + 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/) + end + + it "returns to calling fiber after raise" do + fiber_one = Fiber.new do + Fiber.yield :yield_one + :unreachable + end + + 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 + + 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 + + -> do + f2.raise(RuntimeError, "Expected error") + end.should raise_error(RuntimeError, "Expected error") + end + + it "raises on itself" do + -> do + Fiber.current.raise(RuntimeError, "Expected error") + end.should raise_error(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_error(RuntimeError, "Expected error") + end + end +end + + +describe "Fiber#raise" do + it "transfers and raises on a transferring fiber" do + root = Fiber.current + fiber = Fiber.new { root.transfer } + fiber.transfer + -> { fiber.raise "msg" }.should raise_error(RuntimeError, "msg") + end +end diff --git a/spec/ruby/core/fiber/resume_spec.rb b/spec/ruby/core/fiber/resume_spec.rb new file mode 100644 index 0000000000..4b20f4b4bf --- /dev/null +++ b/spec/ruby/core/fiber/resume_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'shared/resume' + +describe "Fiber#resume" do + it_behaves_like :fiber_resume, :resume +end + +describe "Fiber#resume" do + it "runs until Fiber.yield" do + obj = mock('obj') + obj.should_not_receive(:do) + fiber = Fiber.new { 1 + 2; Fiber.yield; obj.do } + fiber.resume + end + + it "resumes from the last call to Fiber.yield on subsequent invocations" do + fiber = Fiber.new { Fiber.yield :first; :second } + fiber.resume.should == :first + fiber.resume.should == :second + end + + it "sets the block parameters to its arguments on the first invocation" do + first = mock('first') + first.should_receive(:arg).with(:first).twice + + fiber = Fiber.new { |arg| first.arg arg; Fiber.yield; first.arg arg; } + fiber.resume :first + fiber.resume :second + end + + 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 + + it "returns control to the calling Fiber if called from one" do + fiber1 = Fiber.new { :fiber1 } + fiber2 = Fiber.new { fiber1.resume; :fiber2 } + fiber2.resume.should == :fiber2 + end + + # Redmine #595 + it "executes the ensure clause" do + code = <<-RUBY + f = Fiber.new do + begin + Fiber.yield + ensure + puts "ensure executed" + end + end + + # The apparent issue is that when Fiber.yield executes, control + # "leaves" the "ensure block" and so the ensure clause should run. But + # control really does NOT leave the ensure block when Fiber.yield + # executes. It merely pauses there. To require ensure to run when a + # Fiber is suspended then makes ensure-in-a-Fiber-context different + # than ensure-in-a-Thread-context and this would be very confusing. + f.resume + + # When we execute the second #resume call, the ensure block DOES exit, + # the ensure clause runs. + f.resume + + exit 0 + RUBY + + 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_error(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..5ee27d1d24 --- /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_error(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 be_true + end + + it "returns the last value encountered on first invocation" do + fiber = Fiber.new { 1+1; true } + fiber.send(@method).should be_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_error + end + + it "raises a FiberError if the Fiber is dead" do + fiber = Fiber.new { true } + fiber.send(@method) + -> { fiber.send(@method) }.should raise_error(FiberError) + end + + it "raises a LocalJumpError if the block includes a return statement" do + fiber = Fiber.new { return; } + -> { fiber.send(@method) }.should raise_error(LocalJumpError) + end + + it "raises a LocalJumpError if the block includes a break statement" do + fiber = Fiber.new { break; } + -> { fiber.send(@method) }.should raise_error(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..19bfb75e3e --- /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_error(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 be_nil + end + + it "can assign a nil scheduler multiple times" do + Fiber.set_scheduler(nil) + Fiber.set_scheduler(nil) + Fiber.scheduler.should be_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..015caaf3bb --- /dev/null +++ b/spec/ruby/core/fiber/storage_spec.rb @@ -0,0 +1,181 @@ +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_error(TypeError) + end + + it "cannot create a fiber with a frozen hash as storage" do + -> { Fiber.new(storage: {life: 43}.freeze) {} }.should raise_error(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_error(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_error(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_error(TypeError) + end + + it "can't set the storage of the fiber to a frozen hash" do + -> { Fiber.current.storage = {life: 43}.freeze }.should raise_error(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_error(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 be_nil + end + + it "returns nil if the current fiber has no storage" do + Fiber.new { Fiber[:life] }.resume.should be_nil + end + + ruby_version_is "3.2.3" do + 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_error(TypeError) + end + 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_error(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_error(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_error(TypeError) + end + + ruby_version_is "3.3" do + 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 +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..238721475d --- /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_error(FiberError) + end + + it "raises a FiberError when transferring to a Fiber which resumes itself" do + fiber = Fiber.new { fiber.resume } + -> { fiber.transfer }.should raise_error(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 new file mode 100644 index 0000000000..b010912c87 --- /dev/null +++ b/spec/ruby/core/fiber/yield_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Fiber.yield" do + it "passes control to the Fiber's caller" do + step = 0 + fiber = Fiber.new { step = 1; Fiber.yield; step = 2; Fiber.yield; step = 3 } + fiber.resume + step.should == 1 + fiber.resume + step.should == 2 + end + + it "returns its arguments to the caller" do + fiber = Fiber.new { true; Fiber.yield :glark; true } + fiber.resume.should == :glark + fiber.resume + end + + 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 + end + + it "returns to the Fiber the value of the #resume call that invoked it" do + fiber = Fiber.new { Fiber.yield.should == :caller } + fiber.resume + fiber.resume :caller + end + + it "does not propagate or reraise a rescued exception" do + fiber = Fiber.new do + begin + raise "an error in a Fiber" + rescue + Fiber.yield :first + end + + :second + end + + fiber.resume.should == :first + fiber.resume.should == :second + end + + it "raises a FiberError if called from the root Fiber" do + ->{ Fiber.yield }.should raise_error(FiberError) + end +end |
