diff options
| author | Benoit Germain <benoit.germain@ubisoft.com> | 2025-03-17 12:34:08 +0100 |
|---|---|---|
| committer | Benoit Germain <benoit.germain@ubisoft.com> | 2025-03-17 12:34:08 +0100 |
| commit | a57690123ae3ce5bdd7e970690f1380e88e4eaf6 (patch) | |
| tree | d526e8f545cef2b1c23978cb9ee5c94dbc9cda2c | |
| parent | d93de7ca51edea911eeecb7c8edcffe77298ed07 (diff) | |
| download | lanes-a57690123ae3ce5bdd7e970690f1380e88e4eaf6.tar.gz lanes-a57690123ae3ce5bdd7e970690f1380e88e4eaf6.tar.bz2 lanes-a57690123ae3ce5bdd7e970690f1380e88e4eaf6.zip | |
Raise a regular Lua error instead of throwing a C++ std::logic_error exception in Universe::UniverseGC
| -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 |
