From 7c040842b09f952e98187b65019cd55176a5ddf4 Mon Sep 17 00:00:00 2001 From: Benoit Germain Date: Fri, 4 Jul 2025 09:18:19 +0200 Subject: Split coro tests in several scripts --- unit_tests/scripts/_utils.lua | 20 +++- unit_tests/scripts/_utils54.lua | 30 ++++++ unit_tests/scripts/coro/cancelling_suspended.lua | 31 ++++++ unit_tests/scripts/coro/collect_yielded_lane.lua | 87 +---------------- unit_tests/scripts/coro/index_suspended.lua | 28 ++++++ unit_tests/scripts/coro/join_suspended.lua | 24 +++++ unit_tests/scripts/coro/linda_in_close_handler.lua | 43 ++++++++ unit_tests/scripts/coro/resume_basics.lua | 40 ++++++++ unit_tests/scripts/coro/yielding_function.lua | 108 --------------------- .../scripts/coro/yielding_in_non_coro_errors.lua | 27 ++++++ 10 files changed, 247 insertions(+), 191 deletions(-) create mode 100644 unit_tests/scripts/_utils54.lua create mode 100644 unit_tests/scripts/coro/cancelling_suspended.lua create mode 100644 unit_tests/scripts/coro/index_suspended.lua create mode 100644 unit_tests/scripts/coro/join_suspended.lua create mode 100644 unit_tests/scripts/coro/linda_in_close_handler.lua create mode 100644 unit_tests/scripts/coro/resume_basics.lua delete mode 100644 unit_tests/scripts/coro/yielding_function.lua create mode 100644 unit_tests/scripts/coro/yielding_in_non_coro_errors.lua (limited to 'unit_tests/scripts') diff --git a/unit_tests/scripts/_utils.lua b/unit_tests/scripts/_utils.lua index d710702..9f46237 100644 --- a/unit_tests/scripts/_utils.lua +++ b/unit_tests/scripts/_utils.lua @@ -68,8 +68,26 @@ local function dump_error_stack(error_reporting_mode_, stack) end end +-- a function that yields back what got in, one element at a time +local yield_one_by_one = function(...) + local PRINT = MAKE_PRINT() + PRINT "In lane" + for _i = 1, select('#', ...) do + local _val = select(_i, ...) + PRINT("yielding #", _i, _val) + local _ack = coroutine.yield(_val) + if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane) + return "cancelled!" + end + -- of course, if we are cancelled, we were not resumed, and yield() didn't return what we expect + assert(_ack == _i) + end + return "bye!" +end + return { MAKE_PRINT = MAKE_PRINT, tables_match = tables_match, - dump_error_stack = dump_error_stack + dump_error_stack = dump_error_stack, + yield_one_by_one = yield_one_by_one } diff --git a/unit_tests/scripts/_utils54.lua b/unit_tests/scripts/_utils54.lua new file mode 100644 index 0000000..a511563 --- /dev/null +++ b/unit_tests/scripts/_utils54.lua @@ -0,0 +1,30 @@ +local utils = require "_utils" + +-- expand _utils module with Lua5.4 specific stuff + +-- a lane body that yields stuff +utils.yielder_with_to_be_closed = function(out_linda_, wait_) + local fixture = assert(require "fixture") + -- here is a to-be-closed variable that, when closed, sends "Closed!" in the "out" slot of the provided linda + local t = setmetatable( + { text = "Closed!" }, { + __close = function(self, err) + if wait_ then + fixture.block_for(wait_) + end + out_linda_:send("out", self.text) + end + } + ) + -- yield forever, but be cancel-friendly + local n = 1 + while true do + coroutine.yield("I yield!", n) + if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane) + return "I am cancelled" + end + n = n + 1 + end +end + +return utils diff --git a/unit_tests/scripts/coro/cancelling_suspended.lua b/unit_tests/scripts/coro/cancelling_suspended.lua new file mode 100644 index 0000000..3a29e55 --- /dev/null +++ b/unit_tests/scripts/coro/cancelling_suspended.lua @@ -0,0 +1,31 @@ +local fixture = require "fixture" +local lanes = require "lanes".configure{on_state_create = fixture.on_state_create} + +local fixture = require "fixture" +lanes.finally(fixture.throwing_finalizer) + +local utils = lanes.require "_utils" +local PRINT = utils.MAKE_PRINT() + +-------------------------------------------------- +-- TEST: cancelling a suspended Lane should end it +-------------------------------------------------- +if true then + -- the generator + local coro_g = lanes.coro("*", utils.yield_one_by_one) + + -- start the lane + local h = coro_g("hello", "world", "!") + repeat until h.status == "suspended" + + -- first cancellation attempt: don't wake the lane + local b, r = h:cancel("soft", 0.5) + -- the lane is still blocked in its suspended state + assert(b == false and r == "timeout" and h.status == "suspended", "got " .. tostring(b) .. " " .. tostring(r) .. " " .. h.status) + + -- cancel the Lane again, this time waking it. it will resume, and yielder()'s will break out of its infinite loop + h:cancel("soft", nil, true) + + -- lane should be done, because it returned cooperatively when detecting a soft cancel + assert(h.status == "done", "got " .. h.status) +end diff --git a/unit_tests/scripts/coro/collect_yielded_lane.lua b/unit_tests/scripts/coro/collect_yielded_lane.lua index 2bc4ae8..2ee58f8 100644 --- a/unit_tests/scripts/coro/collect_yielded_lane.lua +++ b/unit_tests/scripts/coro/collect_yielded_lane.lua @@ -4,72 +4,18 @@ local lanes = require "lanes".configure{on_state_create = fixture.on_state_creat local fixture = require "fixture" lanes.finally(fixture.throwing_finalizer) -local utils = lanes.require "_utils" +-- this test is only for Lua 5.4+ +local utils = lanes.require "_utils54" local PRINT = utils.MAKE_PRINT() --- a lane body that yields stuff -local yielder = function(out_linda_, wait_) - local fixture = assert(require "fixture") - -- here is a to-be-closed variable that, when closed, sends "Closed!" in the "out" slot of the provided linda - local t = setmetatable( - { text = "Closed!" }, { - __close = function(self, err) - if wait_ then - fixture.block_for(wait_) - end - out_linda_:send("out", self.text) - end - } - ) - -- yield forever, but be cancel-friendly - local n = 1 - while true do - coroutine.yield("I yield!", n) - if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane) - return "I am cancelled" - end - n = n + 1 - end -end - local out_linda = lanes.linda() -local test_close = function(what_, f_) - local c = coroutine.create(f_) - for i = 1, 10 do - local t, r1, r2 = coroutine.resume(c, out_linda) -- returns true + - assert(t == true and r1 == "I yield!" and r2 == i, "got " .. tostring(t) .. " " .. tostring(r1) .. " " .. tostring(r2)) - local s = coroutine.status(c) - assert(s == "suspended") - end - local r, s = coroutine.close(c) - assert(r == true and s == nil) - -- the local variable inside the yielder body should be closed - local s, r = out_linda:receive(0, "out") - assert(s == "out" and r == "Closed!", what_ .. " got " .. tostring(s) .. " " .. tostring(r)) -end - ---------------------------------------------------------- --- TEST: first, try the close mechanism outside of a lane ---------------------------------------------------------- -if true then - test_close("base", yielder) -end - ---------------------------------------------------------------- --- TEST: try again with a function obtained through dump/undump ---------------------------------------------------------------- -if true then - -- note this means our yielder implementation can't have upvalues, as they are lost in the process - test_close("dumped", load(string.dump(yielder))) -end - ------------------------------------------------------------------------------ --- TEST: to-be-closed variables are properly closed whzen the lane is collected +-- TEST: to-be-closed variables are properly closed when the lane is collected ------------------------------------------------------------------------------ if true then -- the generator - local coro_g = lanes.coro("*", yielder) + local coro_g = lanes.coro("*", utils.yielder_with_to_be_closed) -- start the lane local h = coro_g(out_linda) @@ -93,7 +39,7 @@ end --------------------------------------------------------------------------------------------------- if true then -- the generator - local coro_g = lanes.coro("*", yielder) + local coro_g = lanes.coro("*", utils.yielder_with_to_be_closed) -- start the lane. The to-be-closed handler will sleep for 1 second local h = coro_g(out_linda, 1) @@ -116,26 +62,3 @@ if true then local s, r = out_linda:receive(0, "out") assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) -- THIS TEST FAILS end - --------------------------------------------------- --- TEST: cancelling a suspended Lane should end it --------------------------------------------------- -if true then - -- the generator - local coro_g = lanes.coro("*", yielder) - - -- start the lane - local h = coro_g(out_linda) - repeat until h.status == "suspended" - - -- first cancellation attempt: don't wake the lane - local b, r = h:cancel("soft", 0.5) - -- the lane is still blocked in its suspended state - assert(b == false and r == "timeout" and h.status == "suspended", "got " .. tostring(b) .. " " .. tostring(r) .. " " .. h.status) - - -- cancel the Lane again, this time waking it. it will resume, and yielder()'s will break out of its infinite loop - h:cancel("soft", nil, true) - - -- lane should be done, because it returned cooperatively when detecting a soft cancel - assert(h.status == "done", "got " .. h.status) -end diff --git a/unit_tests/scripts/coro/index_suspended.lua b/unit_tests/scripts/coro/index_suspended.lua new file mode 100644 index 0000000..2cd8c28 --- /dev/null +++ b/unit_tests/scripts/coro/index_suspended.lua @@ -0,0 +1,28 @@ +local lanes = require "lanes" + +local fixture = require "fixture" +lanes.finally(fixture.throwing_finalizer) + +local utils = lanes.require "_utils" +local PRINT = utils.MAKE_PRINT() + +-- the coroutine generator +local coro_g = lanes.coro("*", {name = "auto"}, utils.yield_one_by_one) + +------------------------------------------------------------------------- +-- TEST: if we index a yielded lane, we should get the last yielded value +------------------------------------------------------------------------- +if true then + -- launch coroutine lane + local h = coro_g("hello", "world", "!") + -- read the first yielded value, sending back the expected index + assert(h:resume(1) == "hello") + -- indexing multiple times gives back the same us the same yielded value + local r1 = h[1] + local r2 = h[1] + local r3 = h[1] + assert(r1 == "world" and r2 == "world" and r3 == "world", "got " .. r1 .. " " .. r2 .. " " .. r3) + -- once the lane was indexed, it is no longer resumable (just like after join) + local b, e = pcall(h.resume, h, 2) + assert(b == false and e == "cannot resume non-suspended coroutine Lane") +end diff --git a/unit_tests/scripts/coro/join_suspended.lua b/unit_tests/scripts/coro/join_suspended.lua new file mode 100644 index 0000000..33be406 --- /dev/null +++ b/unit_tests/scripts/coro/join_suspended.lua @@ -0,0 +1,24 @@ +local lanes = require "lanes" + +local fixture = require "fixture" +lanes.finally(fixture.throwing_finalizer) + +local utils = lanes.require "_utils" +local PRINT = utils.MAKE_PRINT() + +-- the coroutine generator +local coro_g = lanes.coro("*", {name = "auto"}, utils.yield_one_by_one) + +--------------------------------------------------- +-- TEST: if we join a yielded lane, the lane aborts +--------------------------------------------------- +if true then + -- launch coroutine lane + local h = coro_g("hello", "world", "!") + -- read the first yielded value, sending back the expected index + assert(h:resume(1) == "hello") + -- join the lane. since it will reach a yield point, it unblocks and ends. last yielded values are returned normally + local b, r = h:join(0.5) + local s = h.status + assert(s == "done" and b == true and r == "world", "got " .. s .. " " .. tostring(b) .. " " .. tostring(r)) +end diff --git a/unit_tests/scripts/coro/linda_in_close_handler.lua b/unit_tests/scripts/coro/linda_in_close_handler.lua new file mode 100644 index 0000000..8636f01 --- /dev/null +++ b/unit_tests/scripts/coro/linda_in_close_handler.lua @@ -0,0 +1,43 @@ +local fixture = require "fixture" +local lanes = require "lanes".configure{on_state_create = fixture.on_state_create} + +local fixture = require "fixture" +lanes.finally(fixture.throwing_finalizer) + +-- this test is only for Lua 5.4+ +local utils = lanes.require "_utils54" +local PRINT = utils.MAKE_PRINT() + +local out_linda = lanes.linda() + +local test_close = function(what_, f_) + local c = coroutine.create(f_) + for i = 1, 10 do + local t, r1, r2 = coroutine.resume(c, out_linda) -- returns true + + assert(t == true and r1 == "I yield!" and r2 == i, "got " .. tostring(t) .. " " .. tostring(r1) .. " " .. tostring(r2)) + local s = coroutine.status(c) + assert(s == "suspended") + end + local r, s = coroutine.close(c) + assert(r == true and s == nil) + -- the local variable inside the yielder body should be closed + local s, r = out_linda:receive(0, "out") + assert(s == "out" and r == "Closed!", what_ .. " got " .. tostring(s) .. " " .. tostring(r)) +end + +--------------------------------------------------------- +-- TEST: first, try the close mechanism outside of a lane +--------------------------------------------------------- +if true then + assert(type(utils.yielder_with_to_be_closed) == "function") + test_close("base", utils.yielder_with_to_be_closed) +end + +--------------------------------------------------------------- +-- TEST: try again with a function obtained through dump/undump +--------------------------------------------------------------- +if true then + -- note this means our yielder implementation can't have upvalues, as they are lost in the process + test_close("dumped", load(string.dump(utils.yielder_with_to_be_closed))) +end + diff --git a/unit_tests/scripts/coro/resume_basics.lua b/unit_tests/scripts/coro/resume_basics.lua new file mode 100644 index 0000000..5b124f5 --- /dev/null +++ b/unit_tests/scripts/coro/resume_basics.lua @@ -0,0 +1,40 @@ +local lanes = require "lanes" + +local fixture = require "fixture" +lanes.finally(fixture.throwing_finalizer) + +local utils = lanes.require "_utils" +local PRINT = utils.MAKE_PRINT() + +-- the coroutine generator +local coro_g = lanes.coro("*", {name = "auto"}, utils.yield_one_by_one) + +------------------------------------------------------------------------------------------------- +-- TEST: we can resume as many times as the lane yields, then read the returned value on indexing +------------------------------------------------------------------------------------------------- +if true then + -- launch coroutine lane + local h = coro_g("hello", "world", "!") + -- read the yielded values, sending back the expected index + assert(h:resume(1) == "hello") + assert(h:resume(2) == "world") + assert(h:resume(3) == "!") + -- the lane return value is available as usual + local r = h[1] + assert(r == "bye!") +end + +--------------------------------------------------------------------------------------------- +-- TEST: we can resume as many times as the lane yields, then read the returned value on join +--------------------------------------------------------------------------------------------- +if true then + -- launch coroutine lane + local h = coro_g("hello", "world", "!") + -- read the yielded values, sending back the expected index + assert(h:resume(1) == "hello") + assert(h:resume(2) == "world") + assert(h:resume(3) == "!") + -- the lane return value is available as usual + local s, r = h:join() + assert(h.status == "done" and s == true and r == "bye!") +end diff --git a/unit_tests/scripts/coro/yielding_function.lua b/unit_tests/scripts/coro/yielding_function.lua deleted file mode 100644 index 6518d1f..0000000 --- a/unit_tests/scripts/coro/yielding_function.lua +++ /dev/null @@ -1,108 +0,0 @@ -local lanes = require "lanes" - -local fixture = require "fixture" -lanes.finally(fixture.throwing_finalizer) - -local utils = lanes.require "_utils" -local PRINT = utils.MAKE_PRINT() - --- a lane coroutine that yields back what got in, one element at a time -local yielder = function(...) - local utils = lanes.require "_utils" - local PRINT = utils.MAKE_PRINT() - PRINT "In lane" - for _i = 1, select('#', ...) do - local _val = select(_i, ...) - PRINT("yielding #", _i, _val) - local _ack = coroutine.yield(_val) - assert(_ack == _i) - end - return "bye!" -end - --------------------------------------------------------------------------------------------------- --- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right? --------------------------------------------------------------------------------------------------- -if true then - local fun_g = lanes.gen("*", { name = 'auto' }, yielder) - local h = fun_g("hello", "world", "!") - local err, status, stack = h:join() - PRINT(err, status, stack) - -- the actual error message is not the same for Lua 5.1 - -- of course, it also has to be different for LuaJIT as well - -- also, LuaJIT prepends a file:line to the actual error message, which Lua5.1 does not. - local msgs = { - ["Lua 5.1"] = jit and "attempt to yield across C-call boundary" or "attempt to yield across metamethod/C-call boundary", - ["Lua 5.2"] = "attempt to yield from outside a coroutine", - ["Lua 5.3"] = "attempt to yield from outside a coroutine", - ["Lua 5.4"] = "attempt to yield from outside a coroutine" - } - local expected_msg = msgs[_VERSION] - PRINT("expected_msg = " .. expected_msg) - assert(err == nil and string.find(status, expected_msg, 1, true) and stack == nil, "status = " .. status) -end - --- the coroutine generator -local coro_g = lanes.coro("*", {name = "auto"}, yielder) - -------------------------------------------------------------------------------------------------- --- TEST: we can resume as many times as the lane yields, then read the returned value on indexing -------------------------------------------------------------------------------------------------- -if true then - -- launch coroutine lane - local h = coro_g("hello", "world", "!") - -- read the yielded values, sending back the expected index - assert(h:resume(1) == "hello") - assert(h:resume(2) == "world") - assert(h:resume(3) == "!") - -- the lane return value is available as usual - local r = h[1] - assert(r == "bye!") -end - ---------------------------------------------------------------------------------------------- --- TEST: we can resume as many times as the lane yields, then read the returned value on join ---------------------------------------------------------------------------------------------- -if true then - -- launch coroutine lane - local h = coro_g("hello", "world", "!") - -- read the yielded values, sending back the expected index - assert(h:resume(1) == "hello") - assert(h:resume(2) == "world") - assert(h:resume(3) == "!") - -- the lane return value is available as usual - local s, r = h:join() - assert(h.status == "done" and s == true and r == "bye!") -end - ---------------------------------------------------- --- TEST: if we join a yielded lane, the lane aborts ---------------------------------------------------- -if true then - -- launch coroutine lane - local h = coro_g("hello", "world", "!") - -- read the first yielded value, sending back the expected index - assert(h:resume(1) == "hello") - -- join the lane. since it will reach a yield point, it unblocks and ends. last yielded values are returned normally - local b, r = h:join(0.5) - local s = h.status - assert(s == "done" and b == true and r == "world", "got " .. s .. " " .. tostring(b) .. " " .. tostring(r)) -end - -------------------------------------------------------------------------- --- TEST: if we index a yielded lane, we should get the last yielded value -------------------------------------------------------------------------- -if true then - -- launch coroutine lane - local h = coro_g("hello", "world", "!") - -- read the first yielded value, sending back the expected index - assert(h:resume(1) == "hello") - -- indexing multiple times gives back the same us the same yielded value - local r1 = h[1] - local r2 = h[1] - local r3 = h[1] - assert(r1 == "world" and r2 == "world" and r3 == "world", "got " .. r1 .. " " .. r2 .. " " .. r3) - -- once the lane was indexed, it is no longer resumable (just like after join) - local b, e = pcall(h.resume, h, 2) - assert(b == false and e == "cannot resume non-suspended coroutine Lane") -end diff --git a/unit_tests/scripts/coro/yielding_in_non_coro_errors.lua b/unit_tests/scripts/coro/yielding_in_non_coro_errors.lua new file mode 100644 index 0000000..88d5fe0 --- /dev/null +++ b/unit_tests/scripts/coro/yielding_in_non_coro_errors.lua @@ -0,0 +1,27 @@ +local lanes = require "lanes" + +local fixture = require "fixture" +lanes.finally(fixture.throwing_finalizer) + +local utils = lanes.require "_utils" +local PRINT = utils.MAKE_PRINT() + +-------------------------------------------------------------------------------------------------- +-- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right? +-------------------------------------------------------------------------------------------------- +local fun_g = lanes.gen("*", { name = 'auto' }, utils.yield_one_by_one) +local h = fun_g("hello", "world", "!") +local err, status, stack = h:join() +PRINT(err, status, stack) +-- the actual error message is not the same for Lua 5.1 +-- of course, it also has to be different for LuaJIT as well +-- also, LuaJIT prepends a file:line to the actual error message, which Lua5.1 does not. +local msgs = { + ["Lua 5.1"] = jit and "attempt to yield across C-call boundary" or "attempt to yield across metamethod/C-call boundary", + ["Lua 5.2"] = "attempt to yield from outside a coroutine", + ["Lua 5.3"] = "attempt to yield from outside a coroutine", + ["Lua 5.4"] = "attempt to yield from outside a coroutine" +} +local expected_msg = msgs[_VERSION] +PRINT("expected_msg = " .. expected_msg) +assert(err == nil and string.find(status, expected_msg, 1, true) and stack == nil, "status = " .. status) -- cgit v1.2.3-55-g6feb