diff options
-rw-r--r-- | CHANGES | 2 | ||||
-rw-r--r-- | docs/index.html | 6 | ||||
-rw-r--r-- | src/lane.cpp | 6 | ||||
-rw-r--r-- | src/lane.hpp | 4 | ||||
-rw-r--r-- | src/universe.cpp | 46 | ||||
-rw-r--r-- | src/universe.hpp | 3 | ||||
-rw-r--r-- | unit_tests/_pch.cpp | 14 | ||||
-rw-r--r-- | unit_tests/lane_tests.cpp | 10 | ||||
-rw-r--r-- | unit_tests/scripts/lane/uncooperative_shutdown.lua | 4 | ||||
-rw-r--r-- | unit_tests/shared.cpp | 82 | ||||
-rw-r--r-- | unit_tests/shared.h | 5 |
11 files changed, 119 insertions, 63 deletions
@@ -14,8 +14,8 @@ CHANGE 2: BGe 27-Nov-24 | |||
14 | - lanes.sleep() accepts a new argument "indefinitely" to block forever (until hard cancellation is received). | 14 | - lanes.sleep() accepts a new argument "indefinitely" to block forever (until hard cancellation is received). |
15 | - function set_debug_threadname() available inside a Lane is renamed lane_threadname(); can now both read and write the name. | 15 | - function set_debug_threadname() available inside a Lane is renamed lane_threadname(); can now both read and write the name. |
16 | - new function lanes.finally(). Installs a function that gets called at Lanes shutdown after attempting to terminate all lanes. | 16 | - new function lanes.finally(). Installs a function that gets called at Lanes shutdown after attempting to terminate all lanes. |
17 | If some lanes still run after the finalizer, Universe::__gc with raise an error or freeze, depending on its return value. | ||
17 | - new function lanes.collectgarbage(), to force a full GC cycle in the keeper states. | 18 | - new function lanes.collectgarbage(), to force a full GC cycle in the keeper states. |
18 | If some lanes still run after the finalizer, Lanes with throw an exception or freeze, depending on its return value. | ||
19 | - Configuration settings: | 19 | - Configuration settings: |
20 | - Boolean parameters only accept boolean values. | 20 | - Boolean parameters only accept boolean values. |
21 | - allocator provider function is called with a string hint to distinguish internal allocations, lane and keeper states. | 21 | - allocator provider function is called with a string hint to distinguish internal allocations, lane and keeper states. |
diff --git a/docs/index.html b/docs/index.html index 28acf3b..d0f3940 100644 --- a/docs/index.html +++ b/docs/index.html | |||
@@ -70,7 +70,7 @@ | |||
70 | </p> | 70 | </p> |
71 | 71 | ||
72 | <p> | 72 | <p> |
73 | This document was revised on 13-Dec-24, and applies to version <tt>4.0.0</tt>. | 73 | This document was revised on 17-Mar-25, and applies to version <tt>4.0.0</tt>. |
74 | </p> | 74 | </p> |
75 | </font> | 75 | </font> |
76 | </center> | 76 | </center> |
@@ -380,6 +380,8 @@ | |||
380 | Sets the duration in seconds Lanes will wait for graceful termination of running lanes at application shutdown. Default is <tt>0.25</tt>.<br /> | 380 | Sets the duration in seconds Lanes will wait for graceful termination of running lanes at application shutdown. Default is <tt>0.25</tt>.<br /> |
381 | Lanes signals all lanes for cancellation with <tt>"soft"</tt>, <tt>"hard"</tt>, and <tt>"all"</tt> modes, in that order. Each attempt has <tt>shutdown_timeout</tt> seconds to succeed before the next one.<br /> | 381 | Lanes signals all lanes for cancellation with <tt>"soft"</tt>, <tt>"hard"</tt>, and <tt>"all"</tt> modes, in that order. Each attempt has <tt>shutdown_timeout</tt> seconds to succeed before the next one.<br /> |
382 | Then there is a last chance at cleanup with <a href="#finally"><tt>lanes.finally()</tt></a>. If some lanes are still running after that point, shutdown will either freeze or throw. It is YOUR responsibility to cleanup properly after yourself. | 382 | Then there is a last chance at cleanup with <a href="#finally"><tt>lanes.finally()</tt></a>. If some lanes are still running after that point, shutdown will either freeze or throw. It is YOUR responsibility to cleanup properly after yourself. |
383 | IMPORTANT: If there are still running lanes at shutdown, an error is raised, which will be propagated by Lua to the handler installed by <tt>lua_setwarnf</tt>. If the finalizer returned a value, this will be used as the error message.<br /> | ||
384 | LANES SHUTDOWN WILL NOT BE COMPLETE IN THAT CASE, AND THE SUBSEQUENT CONSEQUENCES ARE UNDEFINED! | ||
383 | </td> | 385 | </td> |
384 | </tr> | 386 | </tr> |
385 | 387 | ||
@@ -487,7 +489,7 @@ | |||
487 | The finalizer is called unprotected from inside <tt>__gc</tt> metamethod of Lanes's Universe. Therefore, if your finalizer raises an error, Lua rules regarding errors in finalizers apply normally.<br /> | 489 | The finalizer is called unprotected from inside <tt>__gc</tt> metamethod of Lanes's Universe. Therefore, if your finalizer raises an error, Lua rules regarding errors in finalizers apply normally.<br /> |
488 | The installed function is called after all free-running lanes got a chance to terminate (see <a href="#shutdown_timeout"><tt>shutdown_timeout</tt></a>), but before lindas become unusable.<br /> | 490 | The installed function is called after all free-running lanes got a chance to terminate (see <a href="#shutdown_timeout"><tt>shutdown_timeout</tt></a>), but before lindas become unusable.<br /> |
489 | The finalizer receives a single argument, a <tt>bool</tt> indicating whether all Lanes are successfully terminated at that point. It is possible to inspect them with <a href="#tracking">tracking</a>.<br /> | 491 | The finalizer receives a single argument, a <tt>bool</tt> indicating whether all Lanes are successfully terminated at that point. It is possible to inspect them with <a href="#tracking">tracking</a>.<br /> |
490 | If there are still running lanes when the finalizer returns: Lanes will throw a C++ <tt>std::logic_error</tt> if the finalizer returned <tt>"throw"</tt>. Any other value will cause Lanes to freeze forever. | 492 | If there are still running lanes when the finalizer returns: If the finalizer returned <tt>"freeze"</tt>, Lanes will freeze inside the Universe <tt>__gc</tt>. Any other value will cause Lanes to raise it as an error. If there is no return value, a default message will be used. |
491 | </p> | 493 | </p> |
492 | 494 | ||
493 | <hr/> | 495 | <hr/> |
diff --git a/src/lane.cpp b/src/lane.cpp index 7a5c257..5cebdfa 100644 --- a/src/lane.cpp +++ b/src/lane.cpp | |||
@@ -756,6 +756,12 @@ static void lane_main(Lane* const lane_) | |||
756 | } | 756 | } |
757 | } | 757 | } |
758 | 758 | ||
759 | if (lane_->flaggedAfterUniverseGC.load(std::memory_order_relaxed)) { | ||
760 | // let's try not to crash if the lane didn't terminate gracefully and the Universe met its end | ||
761 | // there will be leaks, but what else can we do? | ||
762 | return; | ||
763 | } | ||
764 | |||
759 | if (_errorHandlerCount) { | 765 | if (_errorHandlerCount) { |
760 | lua_remove(_L, 1); // L: retvals|error | 766 | lua_remove(_L, 1); // L: retvals|error |
761 | } | 767 | } |
diff --git a/src/lane.hpp b/src/lane.hpp index 9b678d6..5fe36b6 100644 --- a/src/lane.hpp +++ b/src/lane.hpp | |||
@@ -139,6 +139,10 @@ class Lane final | |||
139 | 139 | ||
140 | ErrorTraceLevel const errorTraceLevel{ Basic }; | 140 | ErrorTraceLevel const errorTraceLevel{ Basic }; |
141 | 141 | ||
142 | // when Universe is collected, and an uncooperative Lane refuses to terminate, this flag becomes true | ||
143 | // in case of crash, that's the Lane's fault! | ||
144 | std::atomic_bool flaggedAfterUniverseGC{ false }; | ||
145 | |||
142 | [[nodiscard]] | 146 | [[nodiscard]] |
143 | static void* operator new(size_t size_, Universe* U_) noexcept { return U_->internalAllocator.alloc(size_); } | 147 | static void* operator new(size_t size_, Universe* U_) noexcept { return U_->internalAllocator.alloc(size_); } |
144 | // can't actually delete the operator because the compiler generates stack unwinding code that could call it in case of exception | 148 | // can't actually delete the operator because the compiler generates stack unwinding code that could call it in case of exception |
diff --git a/src/universe.cpp b/src/universe.cpp index 3da0801..dd7bc4b 100644 --- a/src/universe.cpp +++ b/src/universe.cpp | |||
@@ -209,6 +209,17 @@ static int luaG_provide_protected_allocator(lua_State* const L_) | |||
209 | 209 | ||
210 | // ################################################################################################# | 210 | // ################################################################################################# |
211 | 211 | ||
212 | void Universe::flagDanglingLanes() const | ||
213 | { | ||
214 | std::lock_guard<std::mutex> _guard{ selfdestructMutex }; | ||
215 | Lane* _lane{ selfdestructFirst }; | ||
216 | while (_lane != SELFDESTRUCT_END) { | ||
217 | _lane->flaggedAfterUniverseGC.store(true, std::memory_order_relaxed); | ||
218 | _lane = _lane->selfdestruct_next; | ||
219 | } | ||
220 | } | ||
221 | // ################################################################################################# | ||
222 | |||
212 | // called once at the creation of the universe (therefore L_ is the master Lua state everything originates from) | 223 | // called once at the creation of the universe (therefore L_ is the master Lua state everything originates from) |
213 | // Do I need to disable this when compiling for LuaJIT to prevent issues? | 224 | // Do I need to disable this when compiling for LuaJIT to prevent issues? |
214 | void Universe::initializeAllocatorFunction(lua_State* const L_) | 225 | void Universe::initializeAllocatorFunction(lua_State* const L_) |
@@ -399,6 +410,7 @@ bool Universe::terminateFreeRunningLanes(lua_Duration const shutdownTimeout_, Ca | |||
399 | // ################################################################################################# | 410 | // ################################################################################################# |
400 | 411 | ||
401 | // process end: cancel any still free-running threads | 412 | // process end: cancel any still free-running threads |
413 | // as far as I can tell, this can only by called only from lua_close() | ||
402 | int Universe::UniverseGC(lua_State* const L_) | 414 | int Universe::UniverseGC(lua_State* const L_) |
403 | { | 415 | { |
404 | lua_Duration const _shutdown_timeout{ lua_tonumber(L_, lua_upvalueindex(1)) }; | 416 | lua_Duration const _shutdown_timeout{ lua_tonumber(L_, lua_upvalueindex(1)) }; |
@@ -416,23 +428,37 @@ int Universe::UniverseGC(lua_State* const L_) | |||
416 | kFinalizerRegKey.pushValue(L_); // L_: U finalizer|nil | 428 | kFinalizerRegKey.pushValue(L_); // L_: U finalizer|nil |
417 | if (!lua_isnil(L_, -1)) { | 429 | if (!lua_isnil(L_, -1)) { |
418 | lua_pushboolean(L_, _allLanesTerminated); // L_: U finalizer bool | 430 | lua_pushboolean(L_, _allLanesTerminated); // L_: U finalizer bool |
419 | // no protection. Lua rules for errors in finalizers apply normally | 431 | // no protection. Lua rules for errors in finalizers apply normally: |
420 | lua_call(L_, 1, 1); // L_: U ret|error | 432 | // Lua 5.4: error is propagated in the warn system |
433 | // older: error is swallowed | ||
434 | lua_call(L_, 1, 1); // L_: U msg? | ||
435 | // phew, no error in finalizer, since we reached that point | ||
421 | } | 436 | } |
422 | STACK_CHECK(L_, 2); | ||
423 | 437 | ||
424 | // if some lanes are still running here, we have no other choice than crashing or freezing and let the client figure out what's wrong | 438 | if (lua_isnil(L_, kIdxTop)) { |
425 | bool const _throw{ luaG_tostring(L_, kIdxTop) == "throw" }; | 439 | lua_pop(L_, 1); // L_: U |
426 | lua_pop(L_, 1); // L_: U | 440 | // no finalizer, or it returned no value: push some default message on the stack, in case it is necessary |
441 | luaG_pushstring(L_, "uncooperative lanes detected at shutdown"); // L_: U "msg" | ||
442 | } | ||
443 | STACK_CHECK(L_, 2); | ||
427 | 444 | ||
428 | while (_U->selfdestructFirst != SELFDESTRUCT_END) { | 445 | // now, all remaining lanes are flagged. if they crash because we remove keepers and the Universe from under them, it is their fault |
429 | if (_throw) { | 446 | bool const _detectedUncooperativeLanes{ _U->selfdestructFirst != SELFDESTRUCT_END }; |
430 | throw std::logic_error{ "Some lanes are still running at shutdown" }; | 447 | if (_detectedUncooperativeLanes) { |
448 | _U->flagDanglingLanes(); | ||
449 | if (luaG_tostring(L_, kIdxTop) == "freeze") { | ||
450 | std::this_thread::sleep_until(std::chrono::time_point<std::chrono::steady_clock>::max()); | ||
431 | } else { | 451 | } else { |
432 | std::this_thread::yield(); | 452 | // take the value returned by the finalizer (or our default message) and throw it as an error |
453 | // since we are inside Lua's GCTM, it will be propagated through the warning system (Lua 5.4) or swallowed silently | ||
454 | raise_lua_error(L_); | ||
433 | } | 455 | } |
434 | } | 456 | } |
435 | 457 | ||
458 | // --------------------------------------------------------- | ||
459 | // we don't reach that point if some lanes are still running | ||
460 | // --------------------------------------------------------- | ||
461 | |||
436 | // no need to mutex-protect this as all lanes in the universe are gone at that point | 462 | // no need to mutex-protect this as all lanes in the universe are gone at that point |
437 | Linda::DeleteTimerLinda(L_, std::exchange(_U->timerLinda, nullptr), PK); | 463 | Linda::DeleteTimerLinda(L_, std::exchange(_U->timerLinda, nullptr), PK); |
438 | 464 | ||
diff --git a/src/universe.hpp b/src/universe.hpp index ab06907..2a3085d 100644 --- a/src/universe.hpp +++ b/src/universe.hpp | |||
@@ -106,7 +106,7 @@ class Universe final | |||
106 | LaneTracker tracker; | 106 | LaneTracker tracker; |
107 | 107 | ||
108 | // Protects modifying the selfdestruct chain | 108 | // Protects modifying the selfdestruct chain |
109 | std::mutex selfdestructMutex; | 109 | mutable std::mutex selfdestructMutex; |
110 | 110 | ||
111 | // require() serialization | 111 | // require() serialization |
112 | std::recursive_mutex requireMutex; | 112 | std::recursive_mutex requireMutex; |
@@ -126,6 +126,7 @@ class Universe final | |||
126 | 126 | ||
127 | private: | 127 | private: |
128 | static int UniverseGC(lua_State* L_); | 128 | static int UniverseGC(lua_State* L_); |
129 | void flagDanglingLanes() const; | ||
129 | 130 | ||
130 | public: | 131 | public: |
131 | [[nodiscard]] | 132 | [[nodiscard]] |
diff --git a/unit_tests/_pch.cpp b/unit_tests/_pch.cpp index 189089a..0d37ba4 100644 --- a/unit_tests/_pch.cpp +++ b/unit_tests/_pch.cpp | |||
@@ -1,15 +1 @@ | |||
1 | #include "_pch.hpp" | #include "_pch.hpp" | |
2 | |||
3 | // IMPORTANT INFORMATION: some relative paths coded in the test implementations suppose that the cwd when debugging is $(SolutionDir)Lanes | ||
4 | // Therefore that's what needs to be set in Google Test Adapter 'Working Directory' global setting... | ||
5 | |||
6 | //int main(int argc, char* argv[]) | ||
7 | //{ | ||
8 | // // your setup ... | ||
9 | // | ||
10 | // int result = Catch::Session().run(argc, argv); | ||
11 | // | ||
12 | // // your clean-up... | ||
13 | // | ||
14 | // return result; | ||
15 | //} \ No newline at end of file | ||
diff --git a/unit_tests/lane_tests.cpp b/unit_tests/lane_tests.cpp index 0c4feba..d6ef2e0 100644 --- a/unit_tests/lane_tests.cpp +++ b/unit_tests/lane_tests.cpp | |||
@@ -327,10 +327,10 @@ TEST_CASE("scripted tests." #DIR "." #FILE) \ | |||
327 | } | 327 | } |
328 | 328 | ||
329 | MAKE_TEST_CASE(lane, cooperative_shutdown, AssertNoLuaError) | 329 | MAKE_TEST_CASE(lane, cooperative_shutdown, AssertNoLuaError) |
330 | #if LUAJIT_FLAVOR() == 0 | 330 | #if LUA_VERSION_NUM >= 504 // // warnings are a Lua 5.4 feature |
331 | // TODO: for some reason, even though we throw as expected, the test fails with LuaJIT. To be investigated | 331 | // NOTE: when this test ends, there are resource leaks and a dangling thread |
332 | MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertThrows) | 332 | MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertWarns) |
333 | #endif // LUAJIT_FLAVOR() | 333 | #endif // LUA_VERSION_NUM |
334 | MAKE_TEST_CASE(lane, tasking_basic, AssertNoLuaError) | 334 | MAKE_TEST_CASE(lane, tasking_basic, AssertNoLuaError) |
335 | MAKE_TEST_CASE(lane, tasking_cancelling, AssertNoLuaError) | 335 | MAKE_TEST_CASE(lane, tasking_cancelling, AssertNoLuaError) |
336 | MAKE_TEST_CASE(lane, tasking_comms_criss_cross, AssertNoLuaError) | 336 | MAKE_TEST_CASE(lane, tasking_comms_criss_cross, AssertNoLuaError) |
@@ -350,7 +350,7 @@ TEST_CASE("lanes.scripted tests") | |||
350 | { | 350 | { |
351 | auto const& _testParam = GENERATE( | 351 | auto const& _testParam = GENERATE( |
352 | FileRunnerParam{ PUC_LUA_ONLY("lane/cooperative_shutdown"), TestType::AssertNoLuaError }, // 0 | 352 | FileRunnerParam{ PUC_LUA_ONLY("lane/cooperative_shutdown"), TestType::AssertNoLuaError }, // 0 |
353 | FileRunnerParam{ "lane/uncooperative_shutdown", TestType::AssertThrows }, | 353 | FileRunnerParam{ "lane/uncooperative_shutdown", TestType::AssertWarns }, |
354 | FileRunnerParam{ "lane/tasking_basic", TestType::AssertNoLuaError }, // 2 | 354 | FileRunnerParam{ "lane/tasking_basic", TestType::AssertNoLuaError }, // 2 |
355 | FileRunnerParam{ "lane/tasking_cancelling", TestType::AssertNoLuaError }, // 3 | 355 | FileRunnerParam{ "lane/tasking_cancelling", TestType::AssertNoLuaError }, // 3 |
356 | FileRunnerParam{ "lane/tasking_comms_criss_cross", TestType::AssertNoLuaError }, // 4 | 356 | FileRunnerParam{ "lane/tasking_comms_criss_cross", TestType::AssertNoLuaError }, // 4 |
diff --git a/unit_tests/scripts/lane/uncooperative_shutdown.lua b/unit_tests/scripts/lane/uncooperative_shutdown.lua index 51e5762..89e1ff8 100644 --- a/unit_tests/scripts/lane/uncooperative_shutdown.lua +++ b/unit_tests/scripts/lane/uncooperative_shutdown.lua | |||
@@ -8,7 +8,7 @@ local lanes = require "lanes".configure{shutdown_timeout = 0.001, on_state_creat | |||
8 | -- launch lanes that blocks forever | 8 | -- launch lanes that blocks forever |
9 | local lane = function() | 9 | local lane = function() |
10 | local fixture = require "fixture" | 10 | local fixture = require "fixture" |
11 | fixture.forever() | 11 | fixture.sleep_for() |
12 | end | 12 | end |
13 | 13 | ||
14 | -- the generator | 14 | -- the generator |
@@ -20,5 +20,5 @@ local h1 = g1() | |||
20 | -- wait until the lane is running | 20 | -- wait until the lane is running |
21 | repeat until h1.status == "running" | 21 | repeat until h1.status == "running" |
22 | 22 | ||
23 | -- let the script end, Lanes should throw std::logic_error because the lane did not gracefully terminate | 23 | -- this finalizer returns an error string that telling Universe::__gc will use to raise an error when it detects the uncooperative lane |
24 | lanes.finally(fixture.throwing_finalizer) | 24 | lanes.finally(fixture.throwing_finalizer) |
diff --git a/unit_tests/shared.cpp b/unit_tests/shared.cpp index d139579..2e2af73 100644 --- a/unit_tests/shared.cpp +++ b/unit_tests/shared.cpp | |||
@@ -25,35 +25,19 @@ namespace | |||
25 | STACK_CHECK(L_, 0); | 25 | STACK_CHECK(L_, 0); |
26 | } | 26 | } |
27 | 27 | ||
28 | |||
29 | static std::map<lua_State*, std::atomic_flag> sFinalizerHits; | 28 | static std::map<lua_State*, std::atomic_flag> sFinalizerHits; |
30 | static std::mutex sCallCountsLock; | 29 | static std::mutex sCallCountsLock; |
31 | 30 | ||
32 | // a finalizer that we can detect even after closing the state | 31 | // a finalizer that we can detect even after closing the state |
33 | lua_CFunction sThrowingFinalizer = +[](lua_State* L_) { | 32 | lua_CFunction sFreezingFinalizer = +[](lua_State* const L_) { |
34 | std::lock_guard _guard{ sCallCountsLock }; | 33 | std::lock_guard _guard{ sCallCountsLock }; |
35 | sFinalizerHits[L_].test_and_set(); | 34 | sFinalizerHits[L_].test_and_set(); |
36 | luaG_pushstring(L_, "throw"); | 35 | luaG_pushstring(L_, "freeze"); // just freeze the thread in place so that it can be debugged |
37 | return 1; | 36 | return 1; |
38 | }; | 37 | }; |
39 | 38 | ||
40 | // a finalizer that we can detect even after closing the state | ||
41 | lua_CFunction sYieldingFinalizer = +[](lua_State* L_) { | ||
42 | std::lock_guard _guard{ sCallCountsLock }; | ||
43 | sFinalizerHits[L_].test_and_set(); | ||
44 | return 0; | ||
45 | }; | ||
46 | |||
47 | // a function that runs forever | ||
48 | lua_CFunction sForever = +[](lua_State* L_) { | ||
49 | while (true) { | ||
50 | std::this_thread::yield(); | ||
51 | } | ||
52 | return 0; | ||
53 | }; | ||
54 | |||
55 | // a function that returns immediately (so that LuaJIT issues a function call for it) | 39 | // a function that returns immediately (so that LuaJIT issues a function call for it) |
56 | lua_CFunction sGiveMeBack = +[](lua_State* L_) { | 40 | lua_CFunction sGiveMeBack = +[](lua_State* const L_) { |
57 | return lua_gettop(L_); | 41 | return lua_gettop(L_); |
58 | }; | 42 | }; |
59 | 43 | ||
@@ -74,14 +58,42 @@ namespace | |||
74 | return 0; | 58 | return 0; |
75 | }; | 59 | }; |
76 | 60 | ||
61 | // a function that sleeps for the specified duration (in seconds) | ||
62 | lua_CFunction sSleepFor = +[](lua_State* const L_) { | ||
63 | std::chrono::time_point<std::chrono::steady_clock> _until{ std::chrono::time_point<std::chrono::steady_clock>::max() }; | ||
64 | lua_settop(L_, 1); | ||
65 | if (luaG_type(L_, kIdxTop) == LuaType::NUMBER) { // we don't want to use lua_isnumber() because of autocoercion | ||
66 | lua_Duration const _duration{ lua_tonumber(L_, kIdxTop) }; | ||
67 | if (_duration.count() >= 0.0) { | ||
68 | _until = std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::steady_clock::duration>(_duration); | ||
69 | } else { | ||
70 | raise_luaL_argerror(L_, kIdxTop, "duration cannot be < 0"); | ||
71 | } | ||
72 | |||
73 | } else if (!lua_isnoneornil(L_, 2)) { | ||
74 | raise_luaL_argerror(L_, StackIndex{ 2 }, "incorrect duration type"); | ||
75 | } | ||
76 | std::this_thread::sleep_until(_until); | ||
77 | return 0; | ||
78 | }; | ||
79 | |||
80 | // a finalizer that we can detect even after closing the state | ||
81 | lua_CFunction sThrowingFinalizer = +[](lua_State* const L_) { | ||
82 | std::lock_guard _guard{ sCallCountsLock }; | ||
83 | sFinalizerHits[L_].test_and_set(); | ||
84 | bool const _allLanesTerminated = lua_toboolean(L_, kIdxTop); | ||
85 | luaG_pushstring(L_, "Finalizer%s", _allLanesTerminated ? "" : ": Uncooperative lanes detected"); | ||
86 | return 1; | ||
87 | }; | ||
88 | |||
77 | static luaL_Reg const sFixture[] = { | 89 | static luaL_Reg const sFixture[] = { |
78 | { "forever", sForever }, | 90 | { "freezing_finalizer", sFreezingFinalizer }, |
79 | { "give_me_back()", sGiveMeBack }, | 91 | { "give_me_back()", sGiveMeBack }, |
80 | { "newlightuserdata", sNewLightUserData }, | 92 | { "newlightuserdata", sNewLightUserData }, |
81 | { "newuserdata", sNewUserData }, | 93 | { "newuserdata", sNewUserData }, |
82 | { "on_state_create", sOnStateCreate }, | 94 | { "on_state_create", sOnStateCreate }, |
95 | { "sleep_for", sSleepFor }, | ||
83 | { "throwing_finalizer", sThrowingFinalizer }, | 96 | { "throwing_finalizer", sThrowingFinalizer }, |
84 | { "yielding_finalizer", sYieldingFinalizer }, | ||
85 | { nullptr, nullptr } | 97 | { nullptr, nullptr } |
86 | }; | 98 | }; |
87 | } // namespace local | 99 | } // namespace local |
@@ -448,16 +460,34 @@ FileRunner::FileRunner(std::string_view const& where_) | |||
448 | 460 | ||
449 | void FileRunner::performTest(FileRunnerParam const& testParam_) | 461 | void FileRunner::performTest(FileRunnerParam const& testParam_) |
450 | { | 462 | { |
463 | static constexpr auto _atPanic = [](lua_State* const L_) { | ||
464 | throw std::logic_error("panic!"); | ||
465 | return 0; | ||
466 | }; | ||
467 | |||
468 | #if LUA_VERSION_NUM >= 504 // // warnings are a Lua 5.4 feature | ||
469 | std::string _warnMessage; | ||
470 | static constexpr auto _onWarn = [](void* const opaque_, char const* const msg_, int const tocont_) { | ||
471 | std::string& _warnMessage = *static_cast<std::string*>(opaque_); | ||
472 | _warnMessage += msg_; | ||
473 | }; | ||
474 | #endif // LUA_VERSION_NUM | ||
475 | |||
451 | INFO(testParam_.script); | 476 | INFO(testParam_.script); |
452 | switch (testParam_.test) { | 477 | switch (testParam_.test) { |
453 | case TestType::AssertNoLuaError: | 478 | case TestType::AssertNoLuaError: |
454 | requireSuccess(root, testParam_.script); | 479 | requireSuccess(root, testParam_.script); |
455 | break; | 480 | break; |
456 | case TestType::AssertNoThrow: | 481 | |
457 | REQUIRE_NOTHROW((std::ignore = doFile(root, testParam_.script), close())); | 482 | #if LUA_VERSION_NUM >= 504 // // warnings are a Lua 5.4 feature |
458 | break; | 483 | case TestType::AssertWarns: |
459 | case TestType::AssertThrows: | 484 | lua_atpanic(L, _atPanic); |
460 | REQUIRE_THROWS_AS((std::ignore = doFile(root, testParam_.script), close()), std::logic_error); | 485 | lua_setwarnf(L, _onWarn, &_warnMessage); |
486 | std::ignore = doFile(root, testParam_.script); | ||
487 | close(); | ||
488 | WARN(_warnMessage); | ||
489 | REQUIRE(_warnMessage != std::string_view{}); | ||
461 | break; | 490 | break; |
491 | #endif // LUA_VERSION_NUM | ||
462 | } | 492 | } |
463 | } | 493 | } |
diff --git a/unit_tests/shared.h b/unit_tests/shared.h index 8a84a94..b884df0 100644 --- a/unit_tests/shared.h +++ b/unit_tests/shared.h | |||
@@ -66,8 +66,9 @@ class LuaState | |||
66 | enum class [[nodiscard]] TestType | 66 | enum class [[nodiscard]] TestType |
67 | { | 67 | { |
68 | AssertNoLuaError, | 68 | AssertNoLuaError, |
69 | AssertNoThrow, | 69 | #if LUA_VERSION_NUM >= 504 // warnings are a Lua 5.4 feature |
70 | AssertThrows, | 70 | AssertWarns, |
71 | #endif // LUA_VERSION_NUM | ||
71 | }; | 72 | }; |
72 | 73 | ||
73 | struct FileRunnerParam | 74 | struct FileRunnerParam |