summaryrefslogtreecommitdiff
path: root/test/ruby/test_fiber.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/ruby/test_fiber.rb')
-rw-r--r--test/ruby/test_fiber.rb550
1 files changed, 550 insertions, 0 deletions
diff --git a/test/ruby/test_fiber.rb b/test/ruby/test_fiber.rb
new file mode 100644
index 0000000000..6976bd9742
--- /dev/null
+++ b/test/ruby/test_fiber.rb
@@ -0,0 +1,550 @@
+# frozen_string_literal: false
+require 'test/unit'
+require 'fiber'
+EnvUtil.suppress_warning {require 'continuation'}
+require 'tmpdir'
+
+class TestFiber < Test::Unit::TestCase
+ def test_normal
+ assert_equal(:ok2,
+ Fiber.new{|e|
+ assert_equal(:ok1, e)
+ Fiber.yield :ok2
+ }.resume(:ok1)
+ )
+ assert_equal([:a, :b], Fiber.new{|a, b| [a, b]}.resume(:a, :b))
+ end
+
+ def test_argument
+ assert_equal(4, Fiber.new {|i=4| i}.resume)
+ end
+
+ def test_term
+ assert_equal(:ok, Fiber.new{:ok}.resume)
+ assert_equal([:a, :b, :c, :d, :e],
+ Fiber.new{
+ Fiber.new{
+ Fiber.new{
+ Fiber.new{
+ [:a]
+ }.resume + [:b]
+ }.resume + [:c]
+ }.resume + [:d]
+ }.resume + [:e])
+ end
+
+ def test_many_fibers
+ max = 1000
+ assert_equal(max, max.times{
+ Fiber.new{}
+ })
+ GC.start # force collect created fibers
+ assert_equal(max,
+ max.times{|i|
+ Fiber.new{
+ }.resume
+ }
+ )
+ GC.start # force collect created fibers
+ end
+
+ def test_many_fibers_with_threads
+ assert_normal_exit <<-SRC, timeout: 60
+ max = 1000
+ @cnt = 0
+ (1..100).map{|ti|
+ Thread.new{
+ max.times{|i|
+ Fiber.new{
+ @cnt += 1
+ }.resume
+ }
+ }
+ }.each{|t|
+ t.join
+ }
+ SRC
+ end
+
+ def test_error
+ assert_raise(ArgumentError){
+ Fiber.new # Fiber without block
+ }
+ f = Fiber.new{}
+ Thread.new{
+ assert_raise(FiberError){ # Fiber yielding across thread
+ f.resume
+ }
+ }.join
+ assert_raise(FiberError){
+ f = Fiber.new{}
+ f.resume
+ f.resume
+ }
+ if respond_to?(:callcc)
+ assert_raise(RuntimeError){
+ Fiber.new{
+ @c = callcc{|c| @c = c}
+ }.resume
+ @c.call # cross fiber callcc
+ }
+ end
+ assert_raise(RuntimeError){
+ Fiber.new{
+ raise
+ }.resume
+ }
+ assert_raise(FiberError){
+ Fiber.yield
+ }
+ assert_raise(FiberError){
+ fib = Fiber.new{
+ fib.resume
+ }
+ fib.resume
+ }
+ assert_raise(FiberError){
+ fib = Fiber.new{
+ Fiber.new{
+ fib.resume
+ }.resume
+ }
+ fib.resume
+ }
+ assert_raise(FiberError){
+ fib = Fiber.new{}
+ fib.raise "raise in unborn fiber"
+ }
+ assert_raise(FiberError){
+ fib = Fiber.new{}
+ fib.resume
+ fib.raise "raise in dead fiber"
+ }
+ end
+
+ def test_return
+ assert_raise(LocalJumpError){
+ Fiber.new do
+ return
+ end.resume
+ }
+ end
+
+ def test_throw
+ assert_raise(UncaughtThrowError){
+ Fiber.new do
+ throw :a
+ end.resume
+ }
+ end
+
+ def test_raise
+ assert_raise(ZeroDivisionError){
+ Fiber.new do
+ 1/0
+ end.resume
+ }
+ assert_raise(RuntimeError){
+ fib = Fiber.new{ Fiber.yield }
+ fib.resume
+ fib.raise "raise and propagate"
+ }
+ assert_nothing_raised{
+ fib = Fiber.new do
+ begin
+ Fiber.yield
+ rescue
+ end
+ end
+ fib.resume
+ fib.raise "rescue in fiber"
+ }
+ fib = Fiber.new do
+ begin
+ Fiber.yield
+ rescue
+ Fiber.yield :ok
+ end
+ end
+ fib.resume
+ assert_equal(:ok, fib.raise)
+ end
+
+ def test_raise_transferring_fiber
+ root = Fiber.current
+ fib = Fiber.new { root.transfer }
+ fib.transfer
+ assert_raise(RuntimeError){
+ fib.raise "can raise with transfer: true"
+ }
+ assert_not_predicate(fib, :alive?)
+ end
+
+ def test_transfer
+ ary = []
+ f2 = nil
+ f1 = Fiber.new{
+ ary << f2.transfer(:foo)
+ :ok
+ }
+ f2 = Fiber.new{
+ ary << f1.transfer(:baz)
+ :ng
+ }
+ assert_equal(:ok, f1.transfer)
+ assert_equal([:baz], ary)
+ end
+
+ def test_terminate_transferred_fiber
+ log = []
+ fa1 = fa2 = fb1 = r1 = nil
+
+ fa1 = Fiber.new{
+ fa2 = Fiber.new{
+ log << :fa2_terminate
+ }
+ fa2.resume
+ log << :fa1_terminate
+ }
+ fb1 = Fiber.new{
+ fa1.transfer
+ log << :fb1_terminate
+ }
+
+ r1 = Fiber.new{
+ fb1.transfer
+ log << :r1_terminate
+ }
+
+ r1.resume
+ log << :root_terminate
+
+ assert_equal [:fa2_terminate, :fa1_terminate, :r1_terminate, :root_terminate], log
+ end
+
+ def test_tls
+ #
+ def tvar(var, val)
+ old = Thread.current[var]
+ begin
+ Thread.current[var] = val
+ yield
+ ensure
+ Thread.current[var] = old
+ end
+ end
+
+ fb = Fiber.new {
+ assert_equal(nil, Thread.current[:v]); tvar(:v, :x) {
+ assert_equal(:x, Thread.current[:v]); Fiber.yield
+ assert_equal(:x, Thread.current[:v]); }
+ assert_equal(nil, Thread.current[:v]); Fiber.yield
+ raise # unreachable
+ }
+
+ assert_equal(nil, Thread.current[:v]); tvar(:v,1) {
+ assert_equal(1, Thread.current[:v]); tvar(:v,3) {
+ assert_equal(3, Thread.current[:v]); fb.resume
+ assert_equal(3, Thread.current[:v]); }
+ assert_equal(1, Thread.current[:v]); }
+ assert_equal(nil, Thread.current[:v]); fb.resume
+ assert_equal(nil, Thread.current[:v]);
+ end
+
+ def test_fiber_variables
+ assert_equal "bar", Fiber.new {Fiber[:foo] = "bar"; Fiber[:foo]}.resume
+
+ key = :"#{self.class.name}#.#{self.object_id}"
+ Fiber[key] = 42
+ assert_equal 42, Fiber[key]
+
+ key = Object.new
+ def key.to_str; "foo"; end
+ assert_equal "Bar", Fiber.new {Fiber[key] = "Bar"; Fiber[key]}.resume
+ end
+
+ def test_alive
+ fib = Fiber.new{Fiber.yield}
+ assert_equal(true, fib.alive?)
+ fib.resume
+ assert_equal(true, fib.alive?)
+ fib.resume
+ assert_equal(false, fib.alive?)
+ end
+
+ def test_resume_self
+ f = Fiber.new {f.resume}
+ assert_raise(FiberError, '[ruby-core:23651]') {f.transfer}
+ end
+
+ def test_fiber_transfer_segv
+ assert_normal_exit %q{
+ require 'fiber'
+ f2 = nil
+ f1 = Fiber.new{ f2.resume }
+ f2 = Fiber.new{ f1.resume }
+ f1.transfer
+ }, '[ruby-dev:40833]'
+ assert_normal_exit %q{
+ require 'fiber'
+ Fiber.new{}.resume
+ 1.times{Fiber.current.transfer}
+ }
+ end
+
+ def test_resume_root_fiber
+ Thread.new do
+ assert_raise(FiberError) do
+ Fiber.current.resume
+ end
+ end.join
+ end
+
+ def test_gc_root_fiber
+ bug4612 = '[ruby-core:35891]'
+
+ assert_normal_exit %q{
+ require 'fiber'
+ GC.stress = true
+ Thread.start{ Fiber.current; nil }.join
+ GC.start
+ }, bug4612
+ end
+
+ def test_mark_fiber
+ bug13875 = '[ruby-core:82681]'
+
+ assert_normal_exit %q{
+ GC.stress = true
+ up = 1.upto(10)
+ down = 10.downto(1)
+ up.zip(down) {|a, b| a + b == 11 or fail 'oops'}
+ }, bug13875
+ end
+
+ def test_no_valid_cfp
+ bug5083 = '[ruby-dev:44208]'
+ assert_equal([], Fiber.new(&Module.method(:nesting)).resume, bug5083)
+ assert_instance_of(Class, Fiber.new(&Class.new.method(:undef_method)).resume(:to_s), bug5083)
+ end
+
+ def test_prohibit_transfer_to_resuming_fiber
+ root_fiber = Fiber.current
+
+ assert_raise(FiberError){
+ fiber = Fiber.new{ root_fiber.transfer }
+ fiber.resume
+ }
+
+ fa1 = Fiber.new{
+ _fa2 = Fiber.new{ root_fiber.transfer }
+ }
+ fb1 = Fiber.new{
+ _fb2 = Fiber.new{ root_fiber.transfer }
+ }
+ fa1.transfer
+ fb1.transfer
+
+ assert_raise(FiberError){
+ fa1.transfer
+ }
+ assert_raise(FiberError){
+ fb1.transfer
+ }
+ end
+
+ def test_prohibit_transfer_to_yielding_fiber
+ f1 = f2 = f3 = nil
+
+ f1 = Fiber.new{
+ f2 = Fiber.new{
+ f3 = Fiber.new{
+ p f3: Fiber.yield
+ }
+ f3.resume
+ }
+ f2.resume
+ }
+ f1.resume
+
+ assert_raise(FiberError){ f3.transfer 10 }
+ end
+
+ def test_prohibit_resume_to_transferring_fiber
+ root_fiber = Fiber.current
+
+ assert_raise(FiberError){
+ Fiber.new{
+ root_fiber.resume
+ }.transfer
+ }
+
+ f1 = f2 = nil
+ f1 = Fiber.new do
+ f2.transfer
+ end
+ f2 = Fiber.new do
+ f1.resume # attempt to resume transferring fiber
+ end
+
+ assert_raise(FiberError){
+ f1.transfer
+ }
+ end
+
+
+ def test_fork_from_fiber
+ omit 'fork not supported' unless Process.respond_to?(:fork)
+ pid = nil
+ bug5700 = '[ruby-core:41456]'
+ assert_nothing_raised(bug5700) do
+ Fiber.new do
+ pid = fork do
+ xpid = nil
+ Fiber.new {
+ xpid = fork do
+ # enough to trigger GC on old root fiber
+ count = 1000
+ count.times do
+ Fiber.new {}.transfer
+ Fiber.new { Fiber.yield }
+ end
+ exit!(true)
+ end
+ }.transfer
+ _, status = Process.waitpid2(xpid)
+ exit!(status.success?)
+ end
+ end.resume
+ end
+ pid, status = Process.waitpid2(pid)
+ assert_not_predicate(status, :signaled?, bug5700)
+ assert_predicate(status, :success?, bug5700)
+
+ pid = Fiber.new {fork}.resume
+ pid, status = Process.waitpid2(pid)
+ assert_not_predicate(status, :signaled?)
+ assert_predicate(status, :success?)
+ end
+
+ def test_exit_in_fiber
+ bug5993 = '[ruby-dev:45218]'
+ assert_nothing_raised(bug5993) do
+ Thread.new{ Fiber.new{ Thread.exit }.resume; raise "unreachable" }.join
+ end
+ end
+
+ def test_fatal_in_fiber
+ assert_in_out_err(["-r-test-/fatal", "-e", <<-EOS], "", [], /ok/)
+ Fiber.new{
+ Bug.rb_fatal "ok"
+ }.resume
+ puts :ng # unreachable.
+ EOS
+ end
+
+ def test_separate_lastmatch
+ bug7678 = '[ruby-core:51331]'
+ /a/ =~ "a"
+ m1 = $~
+ m2 = nil
+ Fiber.new do
+ /b/ =~ "b"
+ m2 = $~
+ end.resume
+ assert_equal("b", m2[0])
+ assert_equal(m1, $~, bug7678)
+ end
+
+ def test_separate_lastline
+ bug7678 = '[ruby-core:51331]'
+ $_ = s1 = "outer"
+ s2 = nil
+ Fiber.new do
+ s2 = "inner"
+ end.resume
+ assert_equal("inner", s2)
+ assert_equal(s1, $_, bug7678)
+ end
+
+ def test_new_symbol_proc
+ bug = '[ruby-core:80147] [Bug #13313]'
+ assert_ruby_status([], "#{<<-"begin;"}\n#{<<-'end;'}", bug)
+ begin;
+ exit("1" == Fiber.new(&:to_s).resume(1))
+ end;
+ end
+
+ def test_to_s
+ f = Fiber.new do
+ assert_match(/resumed/, f.to_s)
+ Fiber.yield
+ end
+ assert_match(/created/, f.to_s)
+ f.resume
+ assert_match(/suspended/, f.to_s)
+ f.resume
+ assert_match(/terminated/, f.to_s)
+ assert_match(/resumed/, Fiber.current.to_s)
+ end
+
+ def test_create_fiber_in_new_thread
+ ret = Thread.new{
+ Thread.new{
+ Fiber.new{Fiber.yield :ok}.resume
+ }.value
+ }.value
+ assert_equal :ok, ret, '[Bug #14642]'
+ end
+
+ def test_machine_stack_gc
+ assert_normal_exit <<-RUBY, '[Bug #14561]', timeout: 60
+ enum = Enumerator.new { |y| y << 1 }
+ thread = Thread.new { enum.peek }
+ thread.join
+ sleep 5 # pause until thread cache wait time runs out. Native thread exits.
+ GC.start
+ RUBY
+ end
+
+ def test_fiber_pool_stack_acquire_failure
+ environment = {
+ "RUBY_SHARED_FIBER_POOL_MINIMUM_COUNT" => "0",
+ "RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT" => "128"
+ }
+
+ # This program requires, effectively, at most one fiber stack, since the fiber immediately becomes unreachable.
+ assert_separately([environment], <<~RUBY, timeout: 30)
+ GC.disable
+ count_before = GC.count
+
+ # Create more fibers than the pool can handle (but they become immediately unreachable):
+ assert_nothing_raised do
+ 256.times do
+ Fiber.new{Fiber.yield}.resume
+ end
+ end
+
+ # Major GC should have happened at least once:
+ assert_operator(GC.count, :>, count_before)
+ RUBY
+ end
+
+ def test_fiber_pool_stack_acquire_failure_at_maximum_count
+ environment = {
+ "RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT" => "128"
+ }
+
+ assert_separately([environment], <<~RUBY, timeout: 30)
+ GC.disable
+ fibers = []
+ assert_raise(FiberError) do
+ loop do
+ Fiber.new{fibers << Fiber.current; Fiber.yield}.resume
+ raise "expected FiberError before this" if fibers.size > 128
+ end
+ end
+ assert_operator fibers.size, :>=, 128
+ RUBY
+ end
+end