diff options
Diffstat (limited to 'test/fiber')
-rw-r--r-- | test/fiber/autoload.rb | 3 | ||||
-rw-r--r-- | test/fiber/scheduler.rb | 255 | ||||
-rw-r--r-- | test/fiber/test_address_resolve.rb | 4 | ||||
-rw-r--r-- | test/fiber/test_enumerator.rb | 16 | ||||
-rw-r--r-- | test/fiber/test_io.rb | 122 | ||||
-rw-r--r-- | test/fiber/test_io_buffer.rb | 199 | ||||
-rw-r--r-- | test/fiber/test_mutex.rb | 24 | ||||
-rw-r--r-- | test/fiber/test_process.rb | 42 | ||||
-rw-r--r-- | test/fiber/test_queue.rb | 54 | ||||
-rw-r--r-- | test/fiber/test_ractor.rb | 2 | ||||
-rw-r--r-- | test/fiber/test_scheduler.rb | 79 | ||||
-rw-r--r-- | test/fiber/test_storage.rb | 115 | ||||
-rw-r--r-- | test/fiber/test_thread.rb | 22 |
13 files changed, 884 insertions, 53 deletions
diff --git a/test/fiber/autoload.rb b/test/fiber/autoload.rb new file mode 100644 index 0000000000..dcb27164a7 --- /dev/null +++ b/test/fiber/autoload.rb @@ -0,0 +1,3 @@ +sleep 0.01 +module TestFiberSchedulerAutoload +end diff --git a/test/fiber/scheduler.rb b/test/fiber/scheduler.rb index 8c2fdcb0e0..3926226ca3 100644 --- a/test/fiber/scheduler.rb +++ b/test/fiber/scheduler.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true # This is an example and simplified scheduler for test purposes. -# It is not efficient for a large number of file descriptors as it uses IO.select(). -# Production Fiber schedulers should use epoll/kqueue/etc. +# - It is not efficient for a large number of file descriptors as it uses +# IO.select(). +# - It does not correctly handle multiple calls to `wait` with the same file +# descriptor and overlapping events. +# - Production fiber schedulers should use epoll/kqueue/etc. Consider using the +# [`io-event`](https://github.com/socketry/io-event) gem instead of this +# scheduler if you want something simple to build on. require 'fiber' require 'socket' @@ -14,7 +19,17 @@ rescue LoadError end class Scheduler - def initialize + experimental = Warning[:experimental] + begin + Warning[:experimental] = false + IO::Buffer.new(0) + ensure + Warning[:experimental] = experimental + end + + def initialize(fiber = Fiber.current) + @fiber = fiber + @readable = {} @writable = {} @waiting = {} @@ -22,7 +37,7 @@ class Scheduler @closed = false @lock = Thread::Mutex.new - @blocking = 0 + @blocking = Hash.new.compare_by_identity @ready = [] @urgent = IO.pipe @@ -32,6 +47,10 @@ class Scheduler attr :writable attr :waiting + def transfer + @fiber.transfer + end + def next_timeout _fiber, timeout = @waiting.min_by{|key, value| value} @@ -49,8 +68,8 @@ class Scheduler def run # $stderr.puts [__method__, Fiber.current].inspect - while @readable.any? or @writable.any? or @waiting.any? or @blocking.positive? - # Can only handle file descriptors up to 1024... + while @readable.any? or @writable.any? or @waiting.any? or @blocking.any? + # May only handle file descriptors up to 1024... readable, writable = IO.select(@readable.keys + [@urgent.first], @writable.keys, [], next_timeout) # puts "readable: #{readable}" if readable&.any? @@ -75,7 +94,7 @@ class Scheduler end selected.each do |fiber, events| - fiber.resume(events) + fiber.transfer(events) end if @waiting.any? @@ -85,7 +104,7 @@ class Scheduler waiting.each do |fiber, timeout| if fiber.alive? if timeout <= time - fiber.resume + fiber.transfer else @waiting[fiber] = timeout end @@ -101,16 +120,22 @@ class Scheduler end ready.each do |fiber| - fiber.resume + fiber.transfer end end end end + # A fiber scheduler hook, invoked when the scheduler goes out of scope. def scheduler_close close(true) end + # If the `scheduler_close` hook does not exist, this method `close` will be + # invoked instead when the fiber scheduler goes out of scope. This is legacy + # behaviour, you should almost certainly use `scheduler_close`. The reason for + # this, is `scheduler_close` is called when the scheduler goes out of scope, + # while `close` may be called by the user. def close(internal = false) # $stderr.puts [__method__, Fiber.current].inspect @@ -145,6 +170,7 @@ class Scheduler Process.clock_gettime(Process::CLOCK_MONOTONIC) end + # This hook is invoked by `Timeout.timeout` and related code. def timeout_after(duration, klass, message, &block) fiber = Fiber.current @@ -163,6 +189,7 @@ class Scheduler end end + # This hook is invoked by `Process.wait`, `system`, and backticks. def process_wait(pid, flags) # $stderr.puts [__method__, pid, flags, Fiber.current].inspect @@ -172,21 +199,47 @@ class Scheduler end.value end + # This hook is invoked by `IO#read` and `IO#write` in the case that `io_read` + # and `io_write` hooks are not available. This implementation is not + # completely general, in the sense that calling `io_wait` multiple times with + # the same `io` and `events` will not work, which is okay for tests but not + # for real code. Correct fiber schedulers should not have this limitation. def io_wait(io, events, duration) # $stderr.puts [__method__, io, events, duration, Fiber.current].inspect + fiber = Fiber.current + unless (events & IO::READABLE).zero? - @readable[io] = Fiber.current + @readable[io] = fiber + readable = true end unless (events & IO::WRITABLE).zero? - @writable[io] = Fiber.current + @writable[io] = fiber + writable = true + end + + if duration + @waiting[fiber] = current_time + duration end - Fiber.yield + @fiber.transfer + ensure + @waiting.delete(fiber) if duration + @readable.delete(io) if readable + @writable.delete(io) if writable + end + + # This hook is invoked by `IO.select`. Using a thread ensures that the + # operation does not block the fiber scheduler. + def io_select(...) + # Emulate the operation using a non-blocking thread: + Thread.new do + IO.select(...) + end.value end - # Used for Kernel#sleep and Thread::Mutex#sleep + # This hook is invoked by `Kernel#sleep` and `Thread::Mutex#sleep`. def kernel_sleep(duration = nil) # $stderr.puts [__method__, duration, Fiber.current].inspect @@ -195,32 +248,35 @@ class Scheduler return true end - # Used when blocking on synchronization (Thread::Mutex#lock, - # Thread::Queue#pop, Thread::SizedQueue#push, ...) + # This hook is invoked by blocking options such as `Thread::Mutex#lock`, + # `Thread::Queue#pop` and `Thread::SizedQueue#push`, which are unblocked by + # other threads/fibers. To unblock a blocked fiber, you should call `unblock` + # with the same `blocker` and `fiber` arguments. def block(blocker, timeout = nil) # $stderr.puts [__method__, blocker, timeout].inspect + fiber = Fiber.current + if timeout - @waiting[Fiber.current] = current_time + timeout + @waiting[fiber] = current_time + timeout begin - Fiber.yield + @fiber.transfer ensure # Remove from @waiting in the case #unblock was called before the timeout expired: - @waiting.delete(Fiber.current) + @waiting.delete(fiber) end else - @blocking += 1 + @blocking[fiber] = true begin - Fiber.yield + @fiber.transfer ensure - @blocking -= 1 + @blocking.delete(fiber) end end end - # Used when synchronization wakes up a previously-blocked fiber - # (Thread::Mutex#unlock, Thread::Queue#push, ...). - # This might be called from another thread. + # This method is invoked from a thread or fiber to unblock a fiber that is + # blocked by `block`. It is expected to be thread safe. def unblock(blocker, fiber) # $stderr.puts [__method__, blocker, fiber].inspect # $stderr.puts blocker.backtrace.inspect @@ -234,14 +290,20 @@ class Scheduler io.write_nonblock('.') end + # This hook is invoked by `Fiber.schedule`. Strictly speaking, you should use + # it to create scheduled fibers, but it is not required in practice; + # `Fiber.new` is usually sufficient. def fiber(&block) fiber = Fiber.new(blocking: false, &block) - fiber.resume + fiber.transfer return fiber end + # This hook is invoked by `Addrinfo.getaddrinfo`. Using a thread ensures that + # the operation does not block the fiber scheduler, since `getaddrinfo` is + # usually provided by `libc` and is blocking. def address_resolve(hostname) Thread.new do Addrinfo.getaddrinfo(hostname, nil).map(&:ip_address).uniq @@ -249,6 +311,133 @@ class Scheduler end end +# This scheduler class implements `io_read` and `io_write` hooks which require +# `IO::Buffer`. +class IOBufferScheduler < Scheduler + EAGAIN = -Errno::EAGAIN::Errno + + def io_read(io, buffer, length, offset) + total = 0 + io.nonblock = true + + while true + maximum_size = buffer.size - offset + result = blocking{buffer.read(io, maximum_size, offset)} + + if result > 0 + total += result + offset += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN + if length > 0 + self.io_wait(io, IO::READABLE, nil) + else + return result + end + elsif result < 0 + return result + end + end + + return total + end + + def io_write(io, buffer, length, offset) + total = 0 + io.nonblock = true + + while true + maximum_size = buffer.size - offset + result = blocking{buffer.write(io, maximum_size, offset)} + + if result > 0 + total += result + offset += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN + if length > 0 + self.io_wait(io, IO::WRITABLE, nil) + else + return result + end + elsif result < 0 + return result + end + end + + return total + end + + def io_pread(io, buffer, from, length, offset) + total = 0 + io.nonblock = true + + while true + maximum_size = buffer.size - offset + result = blocking{buffer.pread(io, from, maximum_size, offset)} + + if result > 0 + total += result + offset += result + from += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN + if length > 0 + self.io_wait(io, IO::READABLE, nil) + else + return result + end + elsif result < 0 + return result + end + end + + return total + end + + def io_pwrite(io, buffer, from, length, offset) + total = 0 + io.nonblock = true + + while true + maximum_size = buffer.size - offset + result = blocking{buffer.pwrite(io, from, maximum_size, offset)} + + if result > 0 + total += result + offset += result + from += result + break if total >= length + elsif result == 0 + break + elsif result == EAGAIN + if length > 0 + self.io_wait(io, IO::WRITABLE, nil) + else + return result + end + elsif result < 0 + return result + end + end + + return total + end + + def blocking(&block) + Fiber.blocking(&block) + end +end + +# This scheduler has a broken implementation of `unblock`` in the sense that it +# raises an exception. This is used to test the behavior of the scheduler when +# unblock raises an exception. class BrokenUnblockScheduler < Scheduler def unblock(blocker, fiber) super @@ -257,6 +446,9 @@ class BrokenUnblockScheduler < Scheduler end end +# This scheduler has a broken implementation of `unblock` in the sense that it +# sleeps. This is used to test the behavior of the scheduler when unblock +# messes with the internal thread state in an unexpected way. class SleepingUnblockScheduler < Scheduler # This method is invoked when the thread is exiting. def unblock(blocker, fiber) @@ -266,3 +458,16 @@ class SleepingUnblockScheduler < Scheduler sleep(0.1) end end + +# This scheduler has a broken implementation of `kernel_sleep` in the sense that +# it invokes a blocking sleep which can cause a deadlock in some cases. +class SleepingBlockingScheduler < Scheduler + def kernel_sleep(duration = nil) + # Deliberaly sleep in a blocking state which can trigger a deadlock if the implementation is not correct. + Fiber.blocking{sleep 0.0001} + + self.block(:sleep, duration) + + return true + end +end diff --git a/test/fiber/test_address_resolve.rb b/test/fiber/test_address_resolve.rb index 457221b9b1..09c8db6049 100644 --- a/test/fiber/test_address_resolve.rb +++ b/test/fiber/test_address_resolve.rb @@ -179,7 +179,7 @@ class TestAddressResolve < Test::Unit::TestCase Fiber.set_scheduler scheduler Fiber.schedule do - assert_raise(SocketError) { + assert_raise(Socket::ResolutionError) { Addrinfo.getaddrinfo("non-existing-domain.abc", nil) } end @@ -269,7 +269,7 @@ class TestAddressResolve < Test::Unit::TestCase Fiber.set_scheduler scheduler Fiber.schedule do - result = Socket.getnameinfo(["AF_INET", 80, "example.com"], Socket::NI_NUMERICSERV) + result = Socket.getnameinfo(["AF_INET", 80, "example.com"], Socket::NI_NUMERICSERV | Socket::NI_NUMERICHOST) assert_equal(["1.2.3.4", "80"], result) end diff --git a/test/fiber/test_enumerator.rb b/test/fiber/test_enumerator.rb index cd4ccd1de5..e9410f925c 100644 --- a/test/fiber/test_enumerator.rb +++ b/test/fiber/test_enumerator.rb @@ -6,16 +6,10 @@ class TestFiberEnumerator < Test::Unit::TestCase MESSAGE = "Hello World" def test_read_characters - skip "UNIXSocket is not defined!" unless defined?(UNIXSocket) + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) i, o = UNIXSocket.pair - unless i.nonblock? && o.nonblock? - i.close - o.close - skip "I/O is not non-blocking!" - end - message = String.new thread = Thread.new do @@ -48,4 +42,12 @@ class TestFiberEnumerator < Test::Unit::TestCase assert_predicate(i, :closed?) assert_predicate(o, :closed?) end + + def enumerator_fiber_is_nonblocking + enumerator = Enumerator.new do |yielder| + yielder << Fiber.current.blocking? + end + + assert_equal(false, enumerator.next) + end end diff --git a/test/fiber/test_io.rb b/test/fiber/test_io.rb index ce65a55f78..0e3e086d5a 100644 --- a/test/fiber/test_io.rb +++ b/test/fiber/test_io.rb @@ -6,14 +6,12 @@ class TestFiberIO < Test::Unit::TestCase MESSAGE = "Hello World" def test_read - skip "UNIXSocket is not defined!" unless defined?(UNIXSocket) + omit unless defined?(UNIXSocket) i, o = UNIXSocket.pair - - unless i.nonblock? && o.nonblock? - i.close - o.close - skip "I/O is not non-blocking!" + if RUBY_PLATFORM=~/mswin|mingw/ + i.nonblock = true + o.nonblock = true end message = nil @@ -41,11 +39,15 @@ class TestFiberIO < Test::Unit::TestCase end def test_heavy_read - skip unless defined?(UNIXSocket) + omit unless defined?(UNIXSocket) 16.times.map do Thread.new do i, o = UNIXSocket.pair + if RUBY_PLATFORM=~/mswin|mingw/ + i.nonblock = true + o.nonblock = true + end scheduler = Scheduler.new Fiber.set_scheduler scheduler @@ -64,16 +66,11 @@ class TestFiberIO < Test::Unit::TestCase end def test_epipe_on_read - skip "UNIXSocket is not defined!" unless defined?(UNIXSocket) + omit unless defined?(UNIXSocket) + omit "nonblock=true isn't properly supported on Windows" if RUBY_PLATFORM=~/mswin|mingw/ i, o = UNIXSocket.pair - unless i.nonblock? && o.nonblock? - i.close - o.close - skip "I/O is not non-blocking!" - end - error = nil thread = Thread.new do @@ -140,4 +137,101 @@ class TestFiberIO < Test::Unit::TestCase server.close th.join end + + def test_read_write_blocking + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + i.nonblock = false + o.nonblock = false + + message = nil + + thread = Thread.new do + # This scheduler provides non-blocking `io_read`/`io_write`: + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + message = i.read(20) + i.close + end + + Fiber.schedule do + o.write("Hello World") + o.close + end + end + + thread.join + + assert_equal MESSAGE, message + assert_predicate(i, :closed?) + assert_predicate(o, :closed?) + end + + def test_puts_empty + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + i.nonblock = false + o.nonblock = false + + thread = Thread.new do + # This scheduler provides non-blocking `io_read`/`io_write`: + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + # This was causing a segfault on older Ruby. + o.puts "" + o.puts nil + o.close + end + end + + thread.join + + message = i.read + i.close + + assert_equal $/*2, message + end + + def test_io_select + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + UNIXSocket.pair do |r, w| + result = nil + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + w.write("Hello World") + result = IO.select([r], [w]) + end + end + + thread.join + + assert_equal [[r], [w], []], result + end + end + + def test_backquote + result = nil + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + Fiber.schedule do + result = `#{EnvUtil.rubybin} -e "sleep 0.1;puts %[ok]"` + end + end + thread.join + + assert_equal "ok\n", result + end end diff --git a/test/fiber/test_io_buffer.rb b/test/fiber/test_io_buffer.rb new file mode 100644 index 0000000000..a08b1ce1a9 --- /dev/null +++ b/test/fiber/test_io_buffer.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true +require 'test/unit' +require_relative 'scheduler' + +require 'timeout' + +class TestFiberIOBuffer < Test::Unit::TestCase + MESSAGE = "Hello World" + + def test_read_write_blocking + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + i.nonblock = false + o.nonblock = false + + message = nil + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + message = i.read(20) + i.close + end + + Fiber.schedule do + o.write(MESSAGE) + o.close + end + end + + thread.join + + assert_equal MESSAGE, message + assert_predicate(i, :closed?) + assert_predicate(o, :closed?) + ensure + i&.close + o&.close + end + + def test_timeout_after + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + i.nonblock = false + o.nonblock = false + + message = nil + error = nil + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + Timeout.timeout(0.1) do + message = i.read(20) + end + rescue Timeout::Error => error + # Assertions below. + ensure + i.close + end + end + + thread.join + + assert_nil message + assert_kind_of Timeout::Error, error + ensure + i&.close + o&.close + end + + def test_read_nonblock + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + + message = nil + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + message = i.read_nonblock(20, exception: false) + i.close + end + end + + thread.join + + assert_equal :wait_readable, message + ensure + i&.close + o&.close + end + + def test_write_nonblock + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + o.write_nonblock(MESSAGE, exception: false) + o.close + end + end + + thread.join + + assert_equal MESSAGE, i.read + ensure + i&.close + o&.close + end + + def test_io_buffer_read_write + omit "UNIXSocket is not defined!" unless defined?(UNIXSocket) + + i, o = UNIXSocket.pair + source_buffer = IO::Buffer.for("Hello World!") + destination_buffer = IO::Buffer.new(source_buffer.size) + + # Test non-scheduler code path: + source_buffer.write(o, source_buffer.size) + destination_buffer.read(i, source_buffer.size) + assert_equal source_buffer, destination_buffer + + # Test scheduler code path: + destination_buffer.clear + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + source_buffer.write(o, source_buffer.size) + destination_buffer.read(i, source_buffer.size) + end + end + + thread.join + + assert_equal source_buffer, destination_buffer + ensure + i&.close + o&.close + end + + def nonblockable?(io) + io.nonblock{} + true + rescue + false + end + + def test_io_buffer_pread_pwrite + file = Tempfile.new("test_io_buffer_pread_pwrite") + + omit "Non-blocking file IO is not supported" unless nonblockable?(file) + + source_buffer = IO::Buffer.for("Hello World!") + destination_buffer = IO::Buffer.new(source_buffer.size) + + # Test non-scheduler code path: + source_buffer.pwrite(file, 1, source_buffer.size) + destination_buffer.pread(file, 1, source_buffer.size) + assert_equal source_buffer, destination_buffer + + # Test scheduler code path: + destination_buffer.clear + file.truncate(0) + + thread = Thread.new do + scheduler = IOBufferScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + source_buffer.pwrite(file, 1, source_buffer.size) + destination_buffer.pread(file, 1, source_buffer.size) + end + end + + thread.join + + assert_equal source_buffer, destination_buffer + ensure + file&.close! + end +end diff --git a/test/fiber/test_mutex.rb b/test/fiber/test_mutex.rb index b0655f06a5..2cee2cc235 100644 --- a/test/fiber/test_mutex.rb +++ b/test/fiber/test_mutex.rb @@ -194,7 +194,7 @@ class TestFiberMutex < Test::Unit::TestCase end def test_mutex_deadlock - error_pattern = /No live threads left. Deadlock\?/ + error_pattern = /lock already owned by another fiber/ assert_in_out_err %W[-I#{__dir__} -], <<-RUBY, ['in synchronize'], error_pattern, success: false require 'scheduler' @@ -207,7 +207,7 @@ class TestFiberMutex < Test::Unit::TestCase Fiber.schedule do mutex.synchronize do puts 'in synchronize' - Fiber.yield + scheduler.transfer end end @@ -217,4 +217,24 @@ class TestFiberMutex < Test::Unit::TestCase thread.join RUBY end + + def test_mutex_fiber_deadlock_no_scheduler + thr = Thread.new do + loop do + sleep 1 + end + end + + mutex = Mutex.new + mutex.synchronize do + error = assert_raise ThreadError do + Fiber.new do + mutex.lock + end.resume + end + assert_includes error.message, "deadlock; lock already owned by another fiber belonging to the same thread" + end + ensure + thr&.kill&.join + end end diff --git a/test/fiber/test_process.rb b/test/fiber/test_process.rb index c6583cac9b..a09b070c0a 100644 --- a/test/fiber/test_process.rb +++ b/test/fiber/test_process.rb @@ -3,13 +3,15 @@ require 'test/unit' require_relative 'scheduler' class TestFiberProcess < Test::Unit::TestCase + TRUE_CMD = RUBY_PLATFORM =~ /mswin|mingw/ ? "exit 0" : "true" + def test_process_wait Thread.new do scheduler = Scheduler.new Fiber.set_scheduler scheduler Fiber.schedule do - pid = Process.spawn("true") + pid = Process.spawn(TRUE_CMD) Process.wait(pid) # TODO test that scheduler was invoked. @@ -25,7 +27,7 @@ class TestFiberProcess < Test::Unit::TestCase Fiber.set_scheduler scheduler Fiber.schedule do - system("true") + system(TRUE_CMD) # TODO test that scheduler was invoked (currently it's not). @@ -33,4 +35,40 @@ class TestFiberProcess < Test::Unit::TestCase end end.join end + + def test_system_faulty_process_wait + Thread.new do + scheduler = Scheduler.new + + def scheduler.process_wait(pid, flags) + Fiber.blocking{Process.wait(pid, flags)} + + # Don't return `Process::Status` instance. + return false + end + + Fiber.set_scheduler scheduler + + Fiber.schedule do + assert_raise TypeError do + system(TRUE_CMD) + end + end + end.join + end + + def test_fork + omit 'fork not supported' unless Process.respond_to?(:fork) + Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + pid = Process.fork {} + Process.wait(pid) + + assert_predicate $?, :success? + end + end.join + end end diff --git a/test/fiber/test_queue.rb b/test/fiber/test_queue.rb new file mode 100644 index 0000000000..d78b026f11 --- /dev/null +++ b/test/fiber/test_queue.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +require 'test/unit' +require_relative 'scheduler' + +class TestFiberQueue < Test::Unit::TestCase + def test_pop_with_timeout + queue = Thread::Queue.new + kill = false + result = :unspecified + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler(scheduler) + + Fiber.schedule do + result = queue.pop(timeout: 0.0001) + end + + scheduler.run + end + until thread.join(2) + kill = true + thread.kill + end + + assert_false(kill, 'Getting stuck due to a possible compiler bug.') + assert_nil result + end + + def test_pop_with_timeout_and_value + queue = Thread::Queue.new + queue.push(:something) + kill = false + result = :unspecified + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler(scheduler) + + Fiber.schedule do + result = queue.pop(timeout: 0.0001) + end + + scheduler.run + end + until thread.join(2) + kill = true + thread.kill + end + + assert_false(kill, 'Getting stuck due to a possible compiler bug.') + assert_equal :something, result + end +end diff --git a/test/fiber/test_ractor.rb b/test/fiber/test_ractor.rb index d03455a9f7..3c4ccbd8e5 100644 --- a/test/fiber/test_ractor.rb +++ b/test/fiber/test_ractor.rb @@ -4,7 +4,7 @@ require "fiber" class TestFiberCurrentRactor < Test::Unit::TestCase def setup - skip unless defined? Ractor + omit unless defined? Ractor end def test_ractor_shareable diff --git a/test/fiber/test_scheduler.rb b/test/fiber/test_scheduler.rb index f0f5b79f36..34effad816 100644 --- a/test/fiber/test_scheduler.rb +++ b/test/fiber/test_scheduler.rb @@ -27,6 +27,18 @@ class TestFiberScheduler < Test::Unit::TestCase refute f.blocking? end + def test_fiber_blocking + f = Fiber.new(blocking: false) do + fiber = Fiber.current + refute fiber.blocking? + Fiber.blocking do |_fiber| + assert_equal fiber, _fiber + assert fiber.blocking? + end + end + f.resume + end + def test_closed_at_thread_exit scheduler = Scheduler.new @@ -55,6 +67,7 @@ class TestFiberScheduler < Test::Unit::TestCase def test_close_at_exit assert_in_out_err %W[-I#{__dir__} -], <<-RUBY, ['Running Fiber'], [], success: true require 'scheduler' + Warning[:experimental] = false scheduler = Scheduler.new Fiber.set_scheduler scheduler @@ -103,4 +116,70 @@ class TestFiberScheduler < Test::Unit::TestCase thread.join end + + def test_autoload + 10.times do + Object.autoload(:TestFiberSchedulerAutoload, File.expand_path("autoload.rb", __dir__)) + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + + 10.times do + Fiber.schedule do + Object.const_get(:TestFiberSchedulerAutoload) + end + end + end + + thread.join + ensure + $LOADED_FEATURES.delete(File.expand_path("autoload.rb", __dir__)) + Object.send(:remove_const, :TestFiberSchedulerAutoload) + end + end + + def test_deadlock + mutex = Thread::Mutex.new + condition = Thread::ConditionVariable.new + q = 0.0001 + + signaller = Thread.new do + loop do + mutex.synchronize do + condition.signal + end + sleep q + end + end + + i = 0 + + thread = Thread.new do + scheduler = SleepingBlockingScheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + 10.times do + mutex.synchronize do + condition.wait(mutex) + sleep q + i += 1 + end + end + end + end + + # Wait for 10 seconds at most... if it doesn't finish, it's deadlocked. + thread.join(10) + + # If it's deadlocked, it will never finish, so this will be 0. + assert_equal 10, i + ensure + # Make sure the threads are dead... + thread.kill + signaller.kill + thread.join + signaller.join + end end diff --git a/test/fiber/test_storage.rb b/test/fiber/test_storage.rb new file mode 100644 index 0000000000..3726decbdb --- /dev/null +++ b/test/fiber/test_storage.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require 'test/unit' + +class TestFiberStorage < Test::Unit::TestCase + def test_storage + Fiber.new do + Fiber[:x] = 10 + assert_kind_of Hash, Fiber.current.storage + assert_predicate Fiber.current.storage, :any? + end.resume + end + + def test_storage_inherited + Fiber.new do + Fiber[:foo] = :bar + + Fiber.new do + assert_equal :bar, Fiber[:foo] + Fiber[:bar] = :baz + end.resume + + assert_nil Fiber[:bar] + end.resume + end + + def test_variable_assignment + Fiber.new do + Fiber[:foo] = :bar + assert_equal :bar, Fiber[:foo] + end.resume + end + + def test_storage_assignment + old, Warning[:experimental] = Warning[:experimental], false + + Fiber.new do + Fiber.current.storage = {foo: :bar} + assert_equal :bar, Fiber[:foo] + end.resume + ensure + Warning[:experimental] = old + end + + def test_storage_only_allow_access_from_same_fiber + old, Warning[:experimental] = Warning[:experimental], false + + f = Fiber.new do + Fiber[:a] = 1 + end + assert_raise(ArgumentError) { f.storage } + assert_raise(ArgumentError) { f.storage = {} } + ensure + Warning[:experimental] = old + end + + def test_inherited_storage + Fiber.new(storage: {foo: :bar}) do + f = Fiber.new do + assert_equal :bar, Fiber[:foo] + end + f.resume + end.resume + end + + def test_enumerator_inherited_storage + Fiber.new do + Fiber[:item] = "Hello World" + + enumerator = Enumerator.new do |out| + out << Fiber.current + out << Fiber[:item] + end + + # The fiber within the enumerator is not equal to the current... + assert_not_equal Fiber.current, enumerator.next + + # But it inherited the storage from the current fiber: + assert_equal "Hello World", enumerator.next + end.resume + end + + def test_thread_inherited_storage + Fiber.new do + Fiber[:x] = 10 + + x = Thread.new do + Fiber[:y] = 20 + Fiber[:x] + end.value + + assert_equal 10, x + assert_equal nil, Fiber[:y] + end.resume + end + + def test_enumerator_count + Fiber.new do + Fiber[:count] = 0 + + enumerator = Enumerator.new do |y| + Fiber[:count] += 1 + y << Fiber[:count] + end + + assert_equal 1, enumerator.next + assert_equal 0, Fiber[:count] + end.resume + end + + def test_storage_assignment_type_error + assert_raise(TypeError) do + Fiber.new(storage: {Object.new => "bar"}) {} + end + end +end diff --git a/test/fiber/test_thread.rb b/test/fiber/test_thread.rb index 5c25c43de2..5e3cc6d0e1 100644 --- a/test/fiber/test_thread.rb +++ b/test/fiber/test_thread.rb @@ -20,6 +20,28 @@ class TestFiberThread < Test::Unit::TestCase assert_equal :done, thread.value end + def test_thread_join_timeout + sleeper = nil + + thread = Thread.new do + scheduler = Scheduler.new + Fiber.set_scheduler scheduler + + Fiber.schedule do + sleeper = Thread.new{sleep} + sleeper.join(0.1) + end + + scheduler.run + end + + thread.join + + assert_predicate sleeper, :alive? + ensure + sleeper&.kill&.join + end + def test_thread_join_implicit sleeping = false finished = false |