singe/thirdparty/copas/tests/timer.lua

236 lines
7.5 KiB
Lua

-- make sure we are pointing to the local copas first
package.path = string.format("../src/?.lua;%s", package.path)
local copas = require "copas"
local socket = require "socket"
local gettime = copas.gettime
local timer = copas.timer
local successes = 0
copas.loop(function()
local count_t1 = 0
local t1 = timer.new({
delay = 0.5,
recurring = true,
params = "hello world",
callback = function(timer_obj, params)
-- let's ensure parameters get passed
assert(params == "hello world", "expected: hello world")
successes = successes + 1 -- 6 to come
count_t1 = count_t1 + 1
print(params .. " " .. count_t1)
end,
})
-- succes count = 6
local t2 = timer.new({
delay = 0.2, -- we'll override this with 0.1 below
recurring = false,
params = {
start_time = gettime()
},
initial_delay = 0.1, -- initial delay, only 0.1
callback = function(timer_obj, params)
assert(gettime() - params.start_time < 0.11, "didn't honour initial delay, or recurred")
print("this seems to go well, and should print only once")
successes = successes + 1 -- 1 to come
end,
})
-- succes count = 7
timer.new({
delay = 3.3, --> allows T1 to run 6 times
callback = function(timer_obj, params)
t1:cancel()
local _, err = t2:cancel()
assert(err == "not armed", "expected t2 to already be stopped")
successes = successes + 1 -- 1 to come
assert(count_t1 == 6, "expected t1 to run 6 times!")
successes = successes + 1 -- 1 to come
timer_obj:cancel() -- cancel myself
end,
})
-- succes count = 9
timer.new({
delay = 0.1,
recurring = true,
callback = function(timer_obj, params)
-- re-arm myself (recurring), should not be possible
local ok, err = timer_obj:arm(1)
assert(err == "already armed", "expected myself to be already armed")
assert(ok == nil, "expected 'ok' to be nil")
print("failed to re-arm a recurring timer, so that's ok")
successes = successes + 1 -- 1 to come
assert(timer_obj:cancel()) -- cancel myself
end,
})
-- succes count = 10
local touched = false
timer.new({
delay = 0.1,
recurring = false,
callback = function(timer_obj, params)
if touched == false then
-- re-arm myself (non-recurring), should be possible
local ok, err = timer_obj:arm(3)
assert(ok == timer_obj)
assert(err == nil, "expected 'err' to be nil")
touched = gettime()
print("re-armed a non-recurring timer, so that's ok")
successes = successes + 1 -- 1 to come
else
print("a re-armed non-recurring timer executed, so that's ok")
successes = successes + 1 -- 1 to come
local t = math.abs(gettime() - touched - 3)
assert(t < 0.01, "expected a 3 second delay for the rearmed timer. Got: "..(gettime() - touched))
successes = successes + 1 -- 1 to come
end
end,
})
-- succes count = 13
local count = 0
local params_in = {}
-- timer shouldn't be cancelled if its handler errors
timer.new({
name = "error-test",
delay = 0.1,
recurring = true,
params = params_in,
errorhandler = function(msg, co, skt)
local errmsg = copas.gettraceback(msg, co, skt)
assert(errmsg:find("error%-test"), "the threadname wasn't found")
assert(errmsg:find("error 1!") or errmsg:find("error 2!"), "the error message wasn't found")
--print(errmsg)
successes = successes + 1
end,
callback = function(timer_obj, params)
assert(params == params_in, "Params table wasn't passed along")
count = count + 1
if count == 2 then
-- 2nd call, so we're done
timer_obj:cancel()
end
error("error "..count.."!")
end,
})
-- succes count = 15
-- Regression test for https://github.com/lunarmodules/copas/issues/206
-- Cancelling a recurring timer while its callback is yielded on socket I/O
-- must let the in-progress callback resume and finish (just without
-- rescheduling), not abandon it mid-flight.
do
local io_invocations = 0
local io_callback_finished = false
local receiver = socket.udp()
receiver:setsockname("127.0.0.1", 0)
local ip, port = receiver:getsockname()
receiver = copas.wrap(receiver)
local io_timer
io_timer = timer.new({
delay = 0.1,
recurring = true,
callback = function()
io_invocations = io_invocations + 1
local data = receiver:receive() -- yields on socket I/O
assert(data == "wakeup", "expected to receive 'wakeup', got: "..tostring(data))
io_callback_finished = true
end,
})
copas.addthread(function()
-- wait until the timer callback is parked waiting on the socket read
while io_invocations == 0 do
copas.pause(0.01)
end
copas.pause(0.05) -- make sure it actually reached the yield point
assert(io_timer:cancel())
local sender = copas.wrap(socket.udp())
sender:sendto("wakeup", ip, port)
copas.pause(0.2) -- allow the in-progress callback to resume and finish
assert(io_callback_finished, "in-progress callback was abandoned after cancel()")
successes = successes + 1 -- 1 to come
assert(io_invocations == 1, "timer rescheduled after being cancelled")
successes = successes + 1 -- 1 to come
end)
end
-- succes count = 17
end)
assert(successes == 17, "number of successes didn't match! got: "..successes)
-- Regression test for https://github.com/lunarmodules/copas/issues/213
-- A recurring timer that cancels-and-rearms itself from within its own
-- callback must not leak the cancelled (old-generation) coroutine into the
-- sleeping heap. Each leaked coroutine would sit there for its full delay,
-- consuming a slot until it (eventually) expires -- forever, for a delay of
-- math.huge, as in the original report.
-- Run in its own isolated loop so the sleeping-heap count isn't affected by
-- other tests' timers.
local leaktest_done = false
copas.loop(function()
local rearm_count = 0
local max_rearms = 5
local baseline = copas.status().timer
local leak_timer
leak_timer = timer.new({
delay = 2, -- long enough that a leaked old-generation coroutine is still parked when checked
initial_delay = 0,
recurring = true,
callback = function(timer_obj)
rearm_count = rearm_count + 1
if rearm_count <= max_rearms then
assert(timer_obj:cancel())
assert(timer_obj:arm(0))
else
assert(timer_obj:cancel())
end
end,
})
-- watchdog: without the fix, the leaked coroutines keep the loop alive
-- until their delay expires (or forever, for math.huge). Bound the
-- failure mode instead of letting the test suite hang.
local watchdog
watchdog = timer.new({
delay = 5,
callback = function()
print("timer leak regression test (issue #213) did not complete within 5 seconds")
os.exit(1)
end,
})
copas.addthread(function()
while rearm_count <= max_rearms do
copas.pause(0.01)
end
copas.pause(0.05) -- let everything settle
assert(leak_timer.cancelled, "expected the timer to be cancelled")
local timer_count = copas.status().timer
assert(timer_count <= baseline + 1,
"leaked timer coroutines detected: expected around "..baseline..", got "..timer_count)
watchdog:cancel()
leaktest_done = true
end)
end)
assert(leaktest_done, "timer leak regression test (issue #213) did not complete")
print("test success!")