Skip to content

Commit

Permalink
Refactor Lifetime Event Loop (crystal-lang#14996)
Browse files Browse the repository at this point in the history
  • Loading branch information
ysbaddaden authored Nov 19, 2024
1 parent 5e02cd4 commit cc30da2
Show file tree
Hide file tree
Showing 81 changed files with 3,079 additions and 8 deletions.
226 changes: 226 additions & 0 deletions spec/std/crystal/evented/arena_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
{% skip_file unless Crystal.has_constant?(:Evented) %}

require "spec"

describe Crystal::Evented::Arena do
describe "#allocate_at?" do
it "yields block when not allocated" do
arena = Crystal::Evented::Arena(Int32).new(32)
pointer = nil
index = nil
called = 0

ret = arena.allocate_at?(0) do |ptr, idx|
pointer = ptr
index = idx
called += 1
end
ret.should eq(index)
called.should eq(1)

ret = arena.allocate_at?(0) { called += 1 }
ret.should be_nil
called.should eq(1)

pointer.should_not be_nil
index.should_not be_nil

arena.get(index.not_nil!) do |ptr|
ptr.should eq(pointer)
end
end

it "allocates up to capacity" do
arena = Crystal::Evented::Arena(Int32).new(32)
indexes = [] of Crystal::Evented::Arena::Index

indexes = 32.times.map do |i|
arena.allocate_at?(i) { |ptr, _| ptr.value = i }
end.to_a

indexes.size.should eq(32)

indexes.each do |index|
arena.get(index.not_nil!) do |pointer|
pointer.should eq(pointer)
pointer.value.should eq(index.not_nil!.index)
end
end
end

it "checks bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
expect_raises(IndexError) { arena.allocate_at?(-1) { } }
expect_raises(IndexError) { arena.allocate_at?(33) { } }
end
end

describe "#get" do
it "returns previously allocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)
pointer = nil

index = arena.allocate_at(30) do |ptr|
pointer = ptr
ptr.value = 654321
end
called = 0

2.times do
arena.get(index.not_nil!) do |ptr|
ptr.should eq(pointer)
ptr.value.should eq(654321)
called += 1
end
end
called.should eq(2)
end

it "can't access unallocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)

expect_raises(RuntimeError) do
arena.get(Crystal::Evented::Arena::Index.new(10, 0)) { }
end
end

it "checks generation" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

index1 = arena.allocate_at(2) { called += 1 }
called.should eq(1)

arena.free(index1) { }
expect_raises(RuntimeError) { arena.get(index1) { } }

index2 = arena.allocate_at(2) { called += 1 }
called.should eq(2)
expect_raises(RuntimeError) { arena.get(index1) { } }

arena.get(index2) { }
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
expect_raises(IndexError) { arena.get(Crystal::Evented::Arena::Index.new(-1, 0)) { } }
expect_raises(IndexError) { arena.get(Crystal::Evented::Arena::Index.new(33, 0)) { } }
end
end

describe "#get?" do
it "returns previously allocated object" do
arena = Crystal::Evented::Arena(Int32).new(32)
pointer = nil

index = arena.allocate_at(30) do |ptr|
pointer = ptr
ptr.value = 654321
end

called = 0
2.times do
ret = arena.get?(index) do |ptr|
ptr.should eq(pointer)
ptr.not_nil!.value.should eq(654321)
called += 1
end
ret.should be_true
end
called.should eq(2)
end

it "can't access unallocated index" do
arena = Crystal::Evented::Arena(Int32).new(32)

called = 0
ret = arena.get?(Crystal::Evented::Arena::Index.new(10, 0)) { called += 1 }
ret.should be_false
called.should eq(0)
end

it "checks generation" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

old_index = arena.allocate_at(2) { }
arena.free(old_index) { }

# not accessible after free:
ret = arena.get?(old_index) { called += 1 }
ret.should be_false
called.should eq(0)

# can be reallocated:
new_index = arena.allocate_at(2) { }

# still not accessible after reallocate:
ret = arena.get?(old_index) { called += 1 }
ret.should be_false
called.should eq(0)

# accessible after reallocate (new index):
ret = arena.get?(new_index) { called += 1 }
ret.should be_true
called.should eq(1)
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

arena.get?(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }.should be_false
arena.get?(Crystal::Evented::Arena::Index.new(33, 0)) { called += 1 }.should be_false

called.should eq(0)
end
end

describe "#free" do
it "deallocates the object" do
arena = Crystal::Evented::Arena(Int32).new(32)

index1 = arena.allocate_at(3) { |ptr| ptr.value = 123 }
arena.free(index1) { }

index2 = arena.allocate_at(3) { }
index2.should_not eq(index1)

value = nil
arena.get(index2) { |ptr| value = ptr.value }
value.should eq(0)
end

it "checks generation" do
arena = Crystal::Evented::Arena(Int32).new(32)

called = 0
old_index = arena.allocate_at(1) { }

# can free:
arena.free(old_index) { called += 1 }
called.should eq(1)

# can reallocate:
new_index = arena.allocate_at(1) { }

# can't free with invalid index:
arena.free(old_index) { called += 1 }
called.should eq(1)

# but new index can:
arena.free(new_index) { called += 1 }
called.should eq(2)
end

it "checks out of bounds" do
arena = Crystal::Evented::Arena(Int32).new(32)
called = 0

arena.free(Crystal::Evented::Arena::Index.new(-1, 0)) { called += 1 }
arena.free(Crystal::Evented::Arena::Index.new(33, 0)) { called += 1 }

called.should eq(0)
end
end
end
100 changes: 100 additions & 0 deletions spec/std/crystal/evented/poll_descriptor_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{% skip_file unless Crystal.has_constant?(:Evented) %}

require "spec"

class Crystal::Evented::FakeLoop < Crystal::Evented::EventLoop
getter operations = [] of {Symbol, Int32, Crystal::Evented::Arena::Index | Bool}

private def system_run(blocking : Bool) : Nil
end

private def interrupt : Nil
end

protected def system_add(fd : Int32, index : Arena::Index) : Nil
operations << {:add, fd, index}
end

protected def system_del(fd : Int32, closing = true) : Nil
operations << {:del, fd, closing}
end

protected def system_del(fd : Int32, closing = true, &) : Nil
operations << {:del, fd, closing}
end

private def system_set_timer(time : Time::Span?) : Nil
end
end

describe Crystal::Evented::Waiters do
describe "#take_ownership" do
it "associates a poll descriptor to an evloop instance" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)
evloop = Crystal::Evented::FakeLoop.new

pd.take_ownership(evloop, fd, index)
pd.@event_loop.should be(evloop)

evloop.operations.should eq([
{:add, fd, index},
])
end

it "moves a poll descriptor to another evloop instance" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)

evloop1 = Crystal::Evented::FakeLoop.new
evloop2 = Crystal::Evented::FakeLoop.new

pd.take_ownership(evloop1, fd, index)
pd.take_ownership(evloop2, fd, index)

pd.@event_loop.should be(evloop2)

evloop1.operations.should eq([
{:add, fd, index},
{:del, fd, false},
])
evloop2.operations.should eq([
{:add, fd, index},
])
end

it "can't move to the current evloop" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)

evloop = Crystal::Evented::FakeLoop.new

pd.take_ownership(evloop, fd, index)
expect_raises(Exception) { pd.take_ownership(evloop, fd, index) }
end

it "can't move with pending waiters" do
fd = Int32::MAX
pd = Crystal::Evented::PollDescriptor.new
index = Crystal::Evented::Arena::Index.new(fd, 0)
event = Crystal::Evented::Event.new(:io_read, Fiber.current)

evloop1 = Crystal::Evented::FakeLoop.new
pd.take_ownership(evloop1, fd, index)
pd.@readers.add(pointerof(event))

evloop2 = Crystal::Evented::FakeLoop.new
expect_raises(RuntimeError) { pd.take_ownership(evloop2, fd, index) }

pd.@event_loop.should be(evloop1)

evloop1.operations.should eq([
{:add, fd, index},
])
evloop2.operations.should be_empty
end
end
end
Loading

0 comments on commit cc30da2

Please sign in to comment.