singe/thirdparty/timerwheel.lua/spec/timerwheel_spec.lua

460 lines
11 KiB
Lua

_G._TEST = true -- instruct for extra exports for test purposes
local tw = require "timerwheel"
-- make sure luasocket is installed when tesing
assert(
(pcall(require, "socket")),
"LuaSocket must be installed to run the test suite"
)
describe("Timerwheel", function()
local set_time, now
do
local _time
set_time = function(t)
assert(type(t) == "number", "expected number")
assert(t > _time, "new time should be greater than old time")
_time = t
end
now = function()
return _time
end
before_each(function()
_time = 0
end)
end
describe("new()", function()
it("succeeds without options", function()
local wheel = tw.new()
assert.is_table(wheel)
assert.is_function(wheel.step)
end)
it("fails with bad options", function()
local function factory(opts)
return function()
tw.new(opts)
end
end
assert.has_error(function() tw:new() end,
"new should not be called with colon ':' notation")
assert.has_error(factory {precision = -0.3},
"expected 'precision' to be number > 0")
assert.has_error(factory {precision = 0},
"expected 'precision' to be number > 0")
assert.has_error(factory {ringsize = -1},
"expected 'ringsize' to be an integer number > 0")
assert.has_error(factory {ringsize = 0},
"expected 'ringsize' to be an integer number > 0")
assert.has_error(factory {ringsize = 1.5},
"expected 'ringsize' to be an integer number > 0")
assert.has_error(factory {now = "hello"},
"expected 'now' to be a function, got: string")
assert.has_error(factory {err_handler = "hello"},
"expected 'err_handler' to be a function, got: string")
end)
it("succeeds with proper options", function()
local wheel = tw.new {
precision = 0.5,
ringsize = 10,
now = function() end,
err_handler = function() end,
}
assert.is_table(wheel)
assert.is_function(wheel.step)
end)
end)
describe("set() and step()", function()
local wheel, precision, ringsize = nil, 1, 10
before_each(function()
wheel = assert(tw.new({
now = now,
precision = precision,
ringsize = ringsize,
}))
end)
it("sets a timer", function()
local count = 0
local id = wheel:set(0.5, function() count = count + 1 end)
assert.is.equal(-1, id)
assert.is.equal(1, wheel:count())
set_time(1)
wheel:step()
assert.is.equal(1, count)
assert.is.equal(0, wheel:count())
end)
it("sets a timer and passes the argument", function()
local count = 0
local arg = {}
local id = wheel:set(0.5, function(cb_arg)
assert.are.equal(arg, cb_arg)
count = count + 1
end, arg)
assert.is.equal(-1, id)
set_time(1)
wheel:step()
assert.is.equal(1, count)
end)
it("doesn't fail on a callback error", function()
wheel = assert(tw.new({
now = now,
precision = precision,
ringsize = ringsize,
err_handler = function() end, -- drop the error
}))
local id = wheel:set(0.5, function() error() end)
assert.is.equal(-1, id)
set_time(1)
assert.has.no.error(function() wheel:step() end)
end)
it("sets 10 timers", function()
local count = 0
local cb = function() count = count + 1 end
for i = 0, 9 do
local id = wheel:set(i + 0.5, cb)
assert.is.equal(-1-i, id)
end
for i = 1, 10 do
set_time(i)
wheel:step()
assert.is.equal(i, count)
end
end)
it("sets 10 timers and reuses the tables", function()
local count = 0
local cb = function() count = count + 1 end
for i = 0, 9 do
local id = wheel:set(i + 0.5, cb)
assert.is.equal(-1-i, id)
end
for i = 1, 10 do
set_time(i)
wheel:step()
assert.is.equal(i, count)
end
-- do the same again
for i = 0, 9 do
local id = wheel:set(i + 0.5, cb)
assert.is.equal(-1-i-10, id)
end
for i = 11, 20 do
set_time(i)
wheel:step()
assert.is.equal(i, count)
end
-- check reused tables
assert.equal(10, #wheel._tables)
for _, t in ipairs(wheel._tables) do
assert.same({ arg = {}, ids = {}, n = 0 }, t)
end
end)
it("sets a timer in the past", function()
local count = 0
local id = wheel:set(-0.5, function() count = count + 1 end)
assert.is.equal(-1, id)
set_time(1)
wheel:step()
assert.is.equal(1, count)
end)
it("sets a timer on the old edge", function()
local count = 0
local id = wheel:set(0, function() count = count + 1 end)
assert.is.equal(-1, id)
set_time(1)
wheel:step()
assert.is.equal(1, count)
end)
it("sets a timer on the new edge", function()
local count = 0
local id = wheel:set(1, function() count = count + 1 end)
assert.is.equal(-1, id)
set_time(1)
wheel:step()
assert.is.equal(0, count)
set_time(2)
wheel:step()
assert.is.equal(1, count)
end)
it("sets timers over the ring edge", function()
local count = 0
local cb = function() count = count + 1 end
for i = 0, 10 * ringsize - 1 do
local id = wheel:set(i + 0.5, cb)
assert.is.equal(-1-i, id)
end
for i = 1, 10 * ringsize do
set_time(i)
wheel:step()
assert.is.equal(i, count)
end
end)
it("sets timers, skipping over empty rings", function()
local count = 0
local cb = function() count = count + 1 end
local total = 10 * ringsize
for i = 0, total - 1 do
if i >= total/2 then -- skip first half == multiple rings
assert(wheel:set(i + 0.5, cb))
end
end
for i = 1, total do
set_time(i)
wheel:step()
end
assert.is.equal(total/2, count)
end)
it("doesn't execute before edge, but on/after edge", function()
local count = 0
local id = wheel:set(0.5, function() count = count + 1 end)
assert.is.equal(-1, id)
set_time(0.99999)
wheel:step()
assert.is.equal(0, count)
set_time(1)
wheel:step()
assert.is.equal(1, count)
end)
it("callback and args gets GC'ed after executing", function()
local count = 0
local cb = function() count = count + 1 end
local arg = {}
local check_table = setmetatable({
value1 = cb,
value2 = arg,
}, {
__mode = "v"
})
local id = wheel:set(0.5, cb, arg)
assert.is.equal(-1, id)
cb = nil -- luacheck: ignore
arg = nil -- luacheck: ignore
collectgarbage()
collectgarbage()
assert.is.Not.Nil(next(check_table))
set_time(1)
wheel:step()
assert.is.equal(1, count)
collectgarbage()
collectgarbage()
assert.is.Nil(next(check_table))
end)
it("calls the error handler on an error", function()
local err_count = 0
wheel = assert(tw.new({
now = now,
precision = precision,
ringsize = ringsize,
err_handler = function(err) err_count = err_count + 1 end,
}))
local id = wheel:set(0.5, function() error() end)
assert.is.equal(-1, id)
set_time(1)
wheel:step()
assert.is.equal(1, err_count)
end)
end)
describe("peek()", function()
local wheel, precision, ringsize = nil, 1, 10
before_each(function()
wheel = assert(tw.new({
now = now,
precision = precision,
ringsize = ringsize,
}))
end)
it("returns time to execution", function()
assert.is.Nil(wheel:peek()) -- on empty wheel
local count = 0
wheel:set(0.5, function() count = count + 1 end)
assert.is.near(1, wheel:peek(), 0.00001)
assert.is.Nil(wheel:peek(0.999))
set_time(0.9)
assert.is.near(0.1, wheel:peek(), 0.00001)
assert.is.Nil(wheel:peek(0.0999))
end)
it("returns time to execution from last slot", function()
local count = 0
wheel:set(9.5, function() count = count + 1 end)
assert.is.near(10, wheel:peek(), 0.00001)
assert.is.Nil(wheel:peek(9.999))
set_time(9.9)
assert.is.near(0.1, wheel:peek(), 0.00001)
assert.is.Nil(wheel:peek(0.0999))
end)
it("returns time to execution a few (empty) rings ahead", function()
local count = 0
wheel:set(109.5, function() count = count + 1 end)
assert.is.near(110, wheel:peek(), 0.00001)
assert.is.Nil(wheel:peek(109.999))
set_time(109.9)
assert.is.near(0.1, wheel:peek(), 0.00001)
assert.is.Nil(wheel:peek(0.0999))
end)
it("returns time to execution in the past", function()
local count = 0
wheel:set(0.5, function() count = count + 1 end)
set_time(100)
assert.is.near(-99, wheel:peek(), 0.00001)
end)
end)
describe("cancel()", function()
local wheel, precision, ringsize = nil, 1, 10
before_each(function()
wheel = assert(tw.new({
now = now,
precision = precision,
ringsize = ringsize,
}))
end)
it("removes a timer", function()
local count = 0
local id = wheel:set(0.5, function() count = count + 1 end)
assert.is.equal(1, wheel:count())
assert.is.True(wheel:cancel(id))
set_time(1)
wheel:step()
assert.is.equal(0, count)
assert.is.equal(0, wheel:count())
-- check reusable tables
assert.equal(1, #wheel._tables)
assert.same({ arg = {}, ids = {}, n = 0 }, wheel._tables[1])
end)
it("removes a non-existing timer", function()
local id = "something bad"
assert.is.False(wheel:cancel(id))
end)
it("removes timers, leaving holes in the slot", function()
local called = {}
local id1 = wheel:set(0.5, function(arg) called[#called+1] = arg end, "id1")
local _ = wheel:set(0.5, function(arg) called[#called+1] = arg end, "id2")
local id3 = wheel:set(0.5, function(arg) called[#called+1] = arg end, "id3")
local _ = wheel:set(0.5, function(arg) called[#called+1] = arg end, "id4")
local id5 = wheel:set(0.5, function(arg) called[#called+1] = arg end, "id5")
assert.is.equal(5, wheel:count())
assert.is.True(wheel:cancel(id1))
assert.is.True(wheel:cancel(id3))
assert.is.True(wheel:cancel(id5))
set_time(1)
wheel:step()
table.sort(called)
assert.is.equal(2, #called)
assert.is.same({ "id2", "id4" }, called)
-- check reused tables
assert.equal(1, #wheel._tables)
assert.same({ arg = {}, ids = {}, n = 0 }, wheel._tables[1])
end)
it("callback and args gets GC'ed after cancelling", function()
local count = 0
local cb = function() count = count + 1 end
local arg = {}
local check_table = setmetatable({
value1 = cb,
value2 = arg,
}, {
__mode = "v"
})
local id = wheel:set(0.5, cb, arg)
cb = nil -- luacheck: ignore
arg = nil -- luacheck: ignore
collectgarbage()
collectgarbage()
assert.is.Not.Nil(next(check_table))
wheel:cancel(id)
collectgarbage()
collectgarbage()
assert.is.Nil(next(check_table))
end)
end)
end)