aboutsummaryrefslogtreecommitdiff
path: root/unit_tests/scripts
diff options
context:
space:
mode:
authorBenoit Germain <benoit.germain@ubisoft.com>2025-07-04 09:18:19 +0200
committerBenoit Germain <benoit.germain@ubisoft.com>2025-07-04 09:18:19 +0200
commit7c040842b09f952e98187b65019cd55176a5ddf4 (patch)
tree0e5c3ac027429ddad0a4a85961eb97381a0f360a /unit_tests/scripts
parent72d7b36e020fd3f11ec002c110e7340f667d6628 (diff)
downloadlanes-7c040842b09f952e98187b65019cd55176a5ddf4.tar.gz
lanes-7c040842b09f952e98187b65019cd55176a5ddf4.tar.bz2
lanes-7c040842b09f952e98187b65019cd55176a5ddf4.zip
Split coro tests in several scripts
Diffstat (limited to 'unit_tests/scripts')
-rw-r--r--unit_tests/scripts/_utils.lua20
-rw-r--r--unit_tests/scripts/_utils54.lua30
-rw-r--r--unit_tests/scripts/coro/cancelling_suspended.lua31
-rw-r--r--unit_tests/scripts/coro/collect_yielded_lane.lua87
-rw-r--r--unit_tests/scripts/coro/index_suspended.lua28
-rw-r--r--unit_tests/scripts/coro/join_suspended.lua24
-rw-r--r--unit_tests/scripts/coro/linda_in_close_handler.lua43
-rw-r--r--unit_tests/scripts/coro/resume_basics.lua40
-rw-r--r--unit_tests/scripts/coro/yielding_function.lua108
-rw-r--r--unit_tests/scripts/coro/yielding_in_non_coro_errors.lua27
10 files changed, 247 insertions, 191 deletions
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)
68 end 68 end
69end 69end
70 70
71-- a function that yields back what got in, one element at a time
72local yield_one_by_one = function(...)
73 local PRINT = MAKE_PRINT()
74 PRINT "In lane"
75 for _i = 1, select('#', ...) do
76 local _val = select(_i, ...)
77 PRINT("yielding #", _i, _val)
78 local _ack = coroutine.yield(_val)
79 if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane)
80 return "cancelled!"
81 end
82 -- of course, if we are cancelled, we were not resumed, and yield() didn't return what we expect
83 assert(_ack == _i)
84 end
85 return "bye!"
86end
87
71return { 88return {
72 MAKE_PRINT = MAKE_PRINT, 89 MAKE_PRINT = MAKE_PRINT,
73 tables_match = tables_match, 90 tables_match = tables_match,
74 dump_error_stack = dump_error_stack 91 dump_error_stack = dump_error_stack,
92 yield_one_by_one = yield_one_by_one
75} 93}
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 @@
1local utils = require "_utils"
2
3-- expand _utils module with Lua5.4 specific stuff
4
5-- a lane body that yields stuff
6utils.yielder_with_to_be_closed = function(out_linda_, wait_)
7 local fixture = assert(require "fixture")
8 -- here is a to-be-closed variable that, when closed, sends "Closed!" in the "out" slot of the provided linda
9 local t <close> = setmetatable(
10 { text = "Closed!" }, {
11 __close = function(self, err)
12 if wait_ then
13 fixture.block_for(wait_)
14 end
15 out_linda_:send("out", self.text)
16 end
17 }
18 )
19 -- yield forever, but be cancel-friendly
20 local n = 1
21 while true do
22 coroutine.yield("I yield!", n)
23 if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane)
24 return "I am cancelled"
25 end
26 n = n + 1
27 end
28end
29
30return 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 @@
1local fixture = require "fixture"
2local lanes = require "lanes".configure{on_state_create = fixture.on_state_create}
3
4local fixture = require "fixture"
5lanes.finally(fixture.throwing_finalizer)
6
7local utils = lanes.require "_utils"
8local PRINT = utils.MAKE_PRINT()
9
10--------------------------------------------------
11-- TEST: cancelling a suspended Lane should end it
12--------------------------------------------------
13if true then
14 -- the generator
15 local coro_g = lanes.coro("*", utils.yield_one_by_one)
16
17 -- start the lane
18 local h = coro_g("hello", "world", "!")
19 repeat until h.status == "suspended"
20
21 -- first cancellation attempt: don't wake the lane
22 local b, r = h:cancel("soft", 0.5)
23 -- the lane is still blocked in its suspended state
24 assert(b == false and r == "timeout" and h.status == "suspended", "got " .. tostring(b) .. " " .. tostring(r) .. " " .. h.status)
25
26 -- cancel the Lane again, this time waking it. it will resume, and yielder()'s will break out of its infinite loop
27 h:cancel("soft", nil, true)
28
29 -- lane should be done, because it returned cooperatively when detecting a soft cancel
30 assert(h.status == "done", "got " .. h.status)
31end
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
4local fixture = require "fixture" 4local fixture = require "fixture"
5lanes.finally(fixture.throwing_finalizer) 5lanes.finally(fixture.throwing_finalizer)
6 6
7local utils = lanes.require "_utils" 7-- this test is only for Lua 5.4+
8local utils = lanes.require "_utils54"
8local PRINT = utils.MAKE_PRINT() 9local PRINT = utils.MAKE_PRINT()
9 10
10-- a lane body that yields stuff
11local yielder = function(out_linda_, wait_)
12 local fixture = assert(require "fixture")
13 -- here is a to-be-closed variable that, when closed, sends "Closed!" in the "out" slot of the provided linda
14 local t <close> = setmetatable(
15 { text = "Closed!" }, {
16 __close = function(self, err)
17 if wait_ then
18 fixture.block_for(wait_)
19 end
20 out_linda_:send("out", self.text)
21 end
22 }
23 )
24 -- yield forever, but be cancel-friendly
25 local n = 1
26 while true do
27 coroutine.yield("I yield!", n)
28 if cancel_test and cancel_test() then -- cancel_test does not exist when run immediately (not in a Lane)
29 return "I am cancelled"
30 end
31 n = n + 1
32 end
33end
34
35local out_linda = lanes.linda() 11local out_linda = lanes.linda()
36 12
37local test_close = function(what_, f_)
38 local c = coroutine.create(f_)
39 for i = 1, 10 do
40 local t, r1, r2 = coroutine.resume(c, out_linda) -- returns true + <yielded values>
41 assert(t == true and r1 == "I yield!" and r2 == i, "got " .. tostring(t) .. " " .. tostring(r1) .. " " .. tostring(r2))
42 local s = coroutine.status(c)
43 assert(s == "suspended")
44 end
45 local r, s = coroutine.close(c)
46 assert(r == true and s == nil)
47 -- the local variable inside the yielder body should be closed
48 local s, r = out_linda:receive(0, "out")
49 assert(s == "out" and r == "Closed!", what_ .. " got " .. tostring(s) .. " " .. tostring(r))
50end
51
52---------------------------------------------------------
53-- TEST: first, try the close mechanism outside of a lane
54---------------------------------------------------------
55if true then
56 test_close("base", yielder)
57end
58
59---------------------------------------------------------------
60-- TEST: try again with a function obtained through dump/undump
61---------------------------------------------------------------
62if true then
63 -- note this means our yielder implementation can't have upvalues, as they are lost in the process
64 test_close("dumped", load(string.dump(yielder)))
65end
66
67------------------------------------------------------------------------------ 13------------------------------------------------------------------------------
68-- TEST: to-be-closed variables are properly closed whzen the lane is collected 14-- TEST: to-be-closed variables are properly closed when the lane is collected
69------------------------------------------------------------------------------ 15------------------------------------------------------------------------------
70if true then 16if true then
71 -- the generator 17 -- the generator
72 local coro_g = lanes.coro("*", yielder) 18 local coro_g = lanes.coro("*", utils.yielder_with_to_be_closed)
73 19
74 -- start the lane 20 -- start the lane
75 local h = coro_g(out_linda) 21 local h = coro_g(out_linda)
@@ -93,7 +39,7 @@ end
93--------------------------------------------------------------------------------------------------- 39---------------------------------------------------------------------------------------------------
94if true then 40if true then
95 -- the generator 41 -- the generator
96 local coro_g = lanes.coro("*", yielder) 42 local coro_g = lanes.coro("*", utils.yielder_with_to_be_closed)
97 43
98 -- start the lane. The to-be-closed handler will sleep for 1 second 44 -- start the lane. The to-be-closed handler will sleep for 1 second
99 local h = coro_g(out_linda, 1) 45 local h = coro_g(out_linda, 1)
@@ -116,26 +62,3 @@ if true then
116 local s, r = out_linda:receive(0, "out") 62 local s, r = out_linda:receive(0, "out")
117 assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) -- THIS TEST FAILS 63 assert(s == "out" and r == "Closed!", "coro got " .. tostring(s) .. " " .. tostring(r)) -- THIS TEST FAILS
118end 64end
119
120--------------------------------------------------
121-- TEST: cancelling a suspended Lane should end it
122--------------------------------------------------
123if true then
124 -- the generator
125 local coro_g = lanes.coro("*", yielder)
126
127 -- start the lane
128 local h = coro_g(out_linda)
129 repeat until h.status == "suspended"
130
131 -- first cancellation attempt: don't wake the lane
132 local b, r = h:cancel("soft", 0.5)
133 -- the lane is still blocked in its suspended state
134 assert(b == false and r == "timeout" and h.status == "suspended", "got " .. tostring(b) .. " " .. tostring(r) .. " " .. h.status)
135
136 -- cancel the Lane again, this time waking it. it will resume, and yielder()'s will break out of its infinite loop
137 h:cancel("soft", nil, true)
138
139 -- lane should be done, because it returned cooperatively when detecting a soft cancel
140 assert(h.status == "done", "got " .. h.status)
141end
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9-- the coroutine generator
10local coro_g = lanes.coro("*", {name = "auto"}, utils.yield_one_by_one)
11
12-------------------------------------------------------------------------
13-- TEST: if we index a yielded lane, we should get the last yielded value
14-------------------------------------------------------------------------
15if true then
16 -- launch coroutine lane
17 local h = coro_g("hello", "world", "!")
18 -- read the first yielded value, sending back the expected index
19 assert(h:resume(1) == "hello")
20 -- indexing multiple times gives back the same us the same yielded value
21 local r1 = h[1]
22 local r2 = h[1]
23 local r3 = h[1]
24 assert(r1 == "world" and r2 == "world" and r3 == "world", "got " .. r1 .. " " .. r2 .. " " .. r3)
25 -- once the lane was indexed, it is no longer resumable (just like after join)
26 local b, e = pcall(h.resume, h, 2)
27 assert(b == false and e == "cannot resume non-suspended coroutine Lane")
28end
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9-- the coroutine generator
10local coro_g = lanes.coro("*", {name = "auto"}, utils.yield_one_by_one)
11
12---------------------------------------------------
13-- TEST: if we join a yielded lane, the lane aborts
14---------------------------------------------------
15if true then
16 -- launch coroutine lane
17 local h = coro_g("hello", "world", "!")
18 -- read the first yielded value, sending back the expected index
19 assert(h:resume(1) == "hello")
20 -- join the lane. since it will reach a yield point, it unblocks and ends. last yielded values are returned normally
21 local b, r = h:join(0.5)
22 local s = h.status
23 assert(s == "done" and b == true and r == "world", "got " .. s .. " " .. tostring(b) .. " " .. tostring(r))
24end
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 @@
1local fixture = require "fixture"
2local lanes = require "lanes".configure{on_state_create = fixture.on_state_create}
3
4local fixture = require "fixture"
5lanes.finally(fixture.throwing_finalizer)
6
7-- this test is only for Lua 5.4+
8local utils = lanes.require "_utils54"
9local PRINT = utils.MAKE_PRINT()
10
11local out_linda = lanes.linda()
12
13local test_close = function(what_, f_)
14 local c = coroutine.create(f_)
15 for i = 1, 10 do
16 local t, r1, r2 = coroutine.resume(c, out_linda) -- returns true + <yielded values>
17 assert(t == true and r1 == "I yield!" and r2 == i, "got " .. tostring(t) .. " " .. tostring(r1) .. " " .. tostring(r2))
18 local s = coroutine.status(c)
19 assert(s == "suspended")
20 end
21 local r, s = coroutine.close(c)
22 assert(r == true and s == nil)
23 -- the local variable inside the yielder body should be closed
24 local s, r = out_linda:receive(0, "out")
25 assert(s == "out" and r == "Closed!", what_ .. " got " .. tostring(s) .. " " .. tostring(r))
26end
27
28---------------------------------------------------------
29-- TEST: first, try the close mechanism outside of a lane
30---------------------------------------------------------
31if true then
32 assert(type(utils.yielder_with_to_be_closed) == "function")
33 test_close("base", utils.yielder_with_to_be_closed)
34end
35
36---------------------------------------------------------------
37-- TEST: try again with a function obtained through dump/undump
38---------------------------------------------------------------
39if true then
40 -- note this means our yielder implementation can't have upvalues, as they are lost in the process
41 test_close("dumped", load(string.dump(utils.yielder_with_to_be_closed)))
42end
43
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9-- the coroutine generator
10local coro_g = lanes.coro("*", {name = "auto"}, utils.yield_one_by_one)
11
12-------------------------------------------------------------------------------------------------
13-- TEST: we can resume as many times as the lane yields, then read the returned value on indexing
14-------------------------------------------------------------------------------------------------
15if true then
16 -- launch coroutine lane
17 local h = coro_g("hello", "world", "!")
18 -- read the yielded values, sending back the expected index
19 assert(h:resume(1) == "hello")
20 assert(h:resume(2) == "world")
21 assert(h:resume(3) == "!")
22 -- the lane return value is available as usual
23 local r = h[1]
24 assert(r == "bye!")
25end
26
27---------------------------------------------------------------------------------------------
28-- TEST: we can resume as many times as the lane yields, then read the returned value on join
29---------------------------------------------------------------------------------------------
30if true then
31 -- launch coroutine lane
32 local h = coro_g("hello", "world", "!")
33 -- read the yielded values, sending back the expected index
34 assert(h:resume(1) == "hello")
35 assert(h:resume(2) == "world")
36 assert(h:resume(3) == "!")
37 -- the lane return value is available as usual
38 local s, r = h:join()
39 assert(h.status == "done" and s == true and r == "bye!")
40end
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9-- a lane coroutine that yields back what got in, one element at a time
10local yielder = function(...)
11 local utils = lanes.require "_utils"
12 local PRINT = utils.MAKE_PRINT()
13 PRINT "In lane"
14 for _i = 1, select('#', ...) do
15 local _val = select(_i, ...)
16 PRINT("yielding #", _i, _val)
17 local _ack = coroutine.yield(_val)
18 assert(_ack == _i)
19 end
20 return "bye!"
21end
22
23--------------------------------------------------------------------------------------------------
24-- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right?
25--------------------------------------------------------------------------------------------------
26if true then
27 local fun_g = lanes.gen("*", { name = 'auto' }, yielder)
28 local h = fun_g("hello", "world", "!")
29 local err, status, stack = h:join()
30 PRINT(err, status, stack)
31 -- the actual error message is not the same for Lua 5.1
32 -- of course, it also has to be different for LuaJIT as well
33 -- also, LuaJIT prepends a file:line to the actual error message, which Lua5.1 does not.
34 local msgs = {
35 ["Lua 5.1"] = jit and "attempt to yield across C-call boundary" or "attempt to yield across metamethod/C-call boundary",
36 ["Lua 5.2"] = "attempt to yield from outside a coroutine",
37 ["Lua 5.3"] = "attempt to yield from outside a coroutine",
38 ["Lua 5.4"] = "attempt to yield from outside a coroutine"
39 }
40 local expected_msg = msgs[_VERSION]
41 PRINT("expected_msg = " .. expected_msg)
42 assert(err == nil and string.find(status, expected_msg, 1, true) and stack == nil, "status = " .. status)
43end
44
45-- the coroutine generator
46local coro_g = lanes.coro("*", {name = "auto"}, yielder)
47
48-------------------------------------------------------------------------------------------------
49-- TEST: we can resume as many times as the lane yields, then read the returned value on indexing
50-------------------------------------------------------------------------------------------------
51if true then
52 -- launch coroutine lane
53 local h = coro_g("hello", "world", "!")
54 -- read the yielded values, sending back the expected index
55 assert(h:resume(1) == "hello")
56 assert(h:resume(2) == "world")
57 assert(h:resume(3) == "!")
58 -- the lane return value is available as usual
59 local r = h[1]
60 assert(r == "bye!")
61end
62
63---------------------------------------------------------------------------------------------
64-- TEST: we can resume as many times as the lane yields, then read the returned value on join
65---------------------------------------------------------------------------------------------
66if true then
67 -- launch coroutine lane
68 local h = coro_g("hello", "world", "!")
69 -- read the yielded values, sending back the expected index
70 assert(h:resume(1) == "hello")
71 assert(h:resume(2) == "world")
72 assert(h:resume(3) == "!")
73 -- the lane return value is available as usual
74 local s, r = h:join()
75 assert(h.status == "done" and s == true and r == "bye!")
76end
77
78---------------------------------------------------
79-- TEST: if we join a yielded lane, the lane aborts
80---------------------------------------------------
81if true then
82 -- launch coroutine lane
83 local h = coro_g("hello", "world", "!")
84 -- read the first yielded value, sending back the expected index
85 assert(h:resume(1) == "hello")
86 -- join the lane. since it will reach a yield point, it unblocks and ends. last yielded values are returned normally
87 local b, r = h:join(0.5)
88 local s = h.status
89 assert(s == "done" and b == true and r == "world", "got " .. s .. " " .. tostring(b) .. " " .. tostring(r))
90end
91
92-------------------------------------------------------------------------
93-- TEST: if we index a yielded lane, we should get the last yielded value
94-------------------------------------------------------------------------
95if true then
96 -- launch coroutine lane
97 local h = coro_g("hello", "world", "!")
98 -- read the first yielded value, sending back the expected index
99 assert(h:resume(1) == "hello")
100 -- indexing multiple times gives back the same us the same yielded value
101 local r1 = h[1]
102 local r2 = h[1]
103 local r3 = h[1]
104 assert(r1 == "world" and r2 == "world" and r3 == "world", "got " .. r1 .. " " .. r2 .. " " .. r3)
105 -- once the lane was indexed, it is no longer resumable (just like after join)
106 local b, e = pcall(h.resume, h, 2)
107 assert(b == false and e == "cannot resume non-suspended coroutine Lane")
108end
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 @@
1local lanes = require "lanes"
2
3local fixture = require "fixture"
4lanes.finally(fixture.throwing_finalizer)
5
6local utils = lanes.require "_utils"
7local PRINT = utils.MAKE_PRINT()
8
9--------------------------------------------------------------------------------------------------
10-- TEST: if we start a non-coroutine lane with a yielding function, we should get an error, right?
11--------------------------------------------------------------------------------------------------
12local fun_g = lanes.gen("*", { name = 'auto' }, utils.yield_one_by_one)
13local h = fun_g("hello", "world", "!")
14local err, status, stack = h:join()
15PRINT(err, status, stack)
16-- the actual error message is not the same for Lua 5.1
17-- of course, it also has to be different for LuaJIT as well
18-- also, LuaJIT prepends a file:line to the actual error message, which Lua5.1 does not.
19local msgs = {
20 ["Lua 5.1"] = jit and "attempt to yield across C-call boundary" or "attempt to yield across metamethod/C-call boundary",
21 ["Lua 5.2"] = "attempt to yield from outside a coroutine",
22 ["Lua 5.3"] = "attempt to yield from outside a coroutine",
23 ["Lua 5.4"] = "attempt to yield from outside a coroutine"
24}
25local expected_msg = msgs[_VERSION]
26PRINT("expected_msg = " .. expected_msg)
27assert(err == nil and string.find(status, expected_msg, 1, true) and stack == nil, "status = " .. status)