# frozen_string_literal: true # Released under the MIT License. # Copyright, 2011-2017, by Tony Arcieri. # Copyright, 2012, by Logan Bowers. # Copyright, 2013, by Ravil Bayramgalin. # Copyright, 2013, by Tim Carey-Smith. # Copyright, 2015, by Vladimir Kochnev. # Copyright, 2016, by Tiago Cardoso. # Copyright, 2019-2023, by Samuel Williams. # Copyright, 2019, by Jesús Burgos Maciá. # Copyright, 2020, by Thomas Dziedzic. # Copyright, 2021, by Joao Fernandes. require "spec_helper" require "timeout" RSpec.describe NIO::Selector do let(:pair) { IO.pipe } let(:reader) { pair.first } let(:writer) { pair.last } context ".backends" do it "knows all supported backends" do expect(described_class.backends).to be_a Array expect(described_class.backends.first).to be_a Symbol end end context "#initialize" do it "allows explicitly specifying a backend" do |example| backend = described_class.backends.first selector = described_class.new(backend) expect(selector.backend).to eq backend example.reporter.message "Supported backends: #{described_class.backends}" end it "automatically selects a backend if none or nil is specified" do expect(described_class.new.backend).to eq described_class.new(nil).backend end it "raises ArgumentError if given an invalid backend" do expect { described_class.new(:derp) }.to raise_error ArgumentError end it "raises TypeError if given a non-Symbol parameter" do expect { described_class.new(42).to raise_error TypeError } end end context "backend" do it "knows its backend" do |example| expect(subject.backend).to be_a Symbol example.reporter.message "Current backend: #{subject.backend}" end end context "register" do it "registers IO objects" do monitor = subject.register(reader, :r) expect(monitor).not_to be_closed end it "raises TypeError if asked to register non-IO objects" do expect { subject.register(42, :r) }.to raise_exception TypeError end it "raises when asked to register after closing" do subject.close expect { subject.register(reader, :r) }.to raise_exception IOError end end it "knows which IO objects are registered" do subject.register(reader, :r) expect(subject).to be_registered(reader) expect(subject).not_to be_registered(writer) end it "deregisters IO objects" do subject.register(reader, :r) monitor = subject.deregister(reader) expect(subject).not_to be_registered(reader) expect(monitor).to be_closed end it "allows deregistering closed IO objects" do subject.register(reader, :r) reader.close expect do subject.deregister(reader) end.not_to raise_error end it "reports if it is empty" do expect(subject).to be_empty subject.register(reader, :r) expect(subject).not_to be_empty end # This spec might seem a bit silly, but this actually something the # Java NIO API specifically precludes that we need to work around it "allows reregistration of the same IO object across select calls" do monitor = subject.register(reader, :r) writer << "ohai" expect(subject.select).to include monitor expect(reader.read(4)).to eq("ohai") subject.deregister(reader) new_monitor = subject.register(reader, :r) writer << "thar" expect(subject.select).to include new_monitor expect(reader.read(4)).to eq("thar") end context "timeouts" do let(:select_precision) {0.2} let(:timeout) {2.0} let(:payload) {"hi there"} it "waits for timeout when selecting from empty selector" do started_at = Time.now expect(subject.select(timeout)).to be_nil expect(Time.now - started_at).to be_within(select_precision).of(timeout) end it "waits for a timeout when selecting with reader" do monitor = subject.register(reader, :r) writer << payload started_at = Time.now expect(subject.select(timeout)).to include monitor expect(Time.now - started_at).to be_within(select_precision).of(0) reader.read_nonblock(payload.size) started_at = Time.now expect(subject.select(timeout)).to be_nil expect(Time.now - started_at).to be_within(select_precision).of(timeout) end it "raises ArgumentError if given a negative timeout" do subject.register(reader, :r) expect do subject.select(-1) end.to raise_exception(ArgumentError) end end context "wakeup" do let(:select_precision) {0.2} it "wakes up if signaled to from another thread" do subject.register(reader, :r) thread = Thread.new do started_at = Time.now expect(subject.select).to eq [] Time.now - started_at end timeout = 0.1 sleep timeout subject.wakeup expect(thread.value).to be_within(select_precision).of(timeout) end it "raises IOError if asked to wake up a closed selector" do subject.close expect(subject).to be_closed expect { subject.wakeup }.to raise_exception IOError end end context "select" do it "does not block on super small precision intervals" do wait_interval = 1e-4 expect do Timeout.timeout(2) do subject.select(wait_interval) end end.not_to raise_error end it "selects IO objects" do writer << "ohai" unready = IO.pipe.first reader_monitor = subject.register(reader, :r) unready_monitor = subject.register(unready, :r) selected = subject.select(0) expect(selected.size).to eq(1) expect(selected).to include reader_monitor expect(selected).not_to include unready_monitor end it "selects closed IO objects" do monitor = subject.register(reader, :r) expect(subject.select(0)).to be_nil thread = Thread.new { subject.select } Thread.pass while thread.status && thread.status != "sleep" writer.close selected = thread.value expect(selected).to include monitor end it "iterates across selected objects with a block" do readable1, writer = IO.pipe writer << "ohai" readable2, writer = IO.pipe writer << "ohai" unreadable = IO.pipe.first monitor1 = subject.register(readable1, :r) monitor2 = subject.register(readable2, :r) monitor3 = subject.register(unreadable, :r) readables = [] result = subject.select { |monitor| readables << monitor } expect(result).to eq(2) expect(readables).to include monitor1 expect(readables).to include monitor2 expect(readables).not_to include monitor3 end it "raises IOError if asked to select on a closed selector" do subject.close expect { subject.select(0) }.to raise_exception IOError end end it "closes" do subject.close expect(subject).to be_closed end end