aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenoit Germain <benoit.germain@ubisoft.com>2025-03-17 12:34:08 +0100
committerBenoit Germain <benoit.germain@ubisoft.com>2025-03-17 12:34:08 +0100
commita57690123ae3ce5bdd7e970690f1380e88e4eaf6 (patch)
treed526e8f545cef2b1c23978cb9ee5c94dbc9cda2c
parentd93de7ca51edea911eeecb7c8edcffe77298ed07 (diff)
downloadlanes-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--CHANGES2
-rw-r--r--docs/index.html6
-rw-r--r--src/lane.cpp6
-rw-r--r--src/lane.hpp4
-rw-r--r--src/universe.cpp46
-rw-r--r--src/universe.hpp3
-rw-r--r--unit_tests/_pch.cpp14
-rw-r--r--unit_tests/lane_tests.cpp10
-rw-r--r--unit_tests/scripts/lane/uncooperative_shutdown.lua4
-rw-r--r--unit_tests/shared.cpp82
-rw-r--r--unit_tests/shared.h5
11 files changed, 119 insertions, 63 deletions
diff --git a/CHANGES b/CHANGES
index 713249c..7cf28b2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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
212void 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?
214void Universe::initializeAllocatorFunction(lua_State* const L_) 225void 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()
402int Universe::UniverseGC(lua_State* const L_) 414int 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
329MAKE_TEST_CASE(lane, cooperative_shutdown, AssertNoLuaError) 329MAKE_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
332MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertThrows) 332MAKE_TEST_CASE(lane, uncooperative_shutdown, AssertWarns)
333#endif // LUAJIT_FLAVOR() 333#endif // LUA_VERSION_NUM
334MAKE_TEST_CASE(lane, tasking_basic, AssertNoLuaError) 334MAKE_TEST_CASE(lane, tasking_basic, AssertNoLuaError)
335MAKE_TEST_CASE(lane, tasking_cancelling, AssertNoLuaError) 335MAKE_TEST_CASE(lane, tasking_cancelling, AssertNoLuaError)
336MAKE_TEST_CASE(lane, tasking_comms_criss_cross, AssertNoLuaError) 336MAKE_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
9local lane = function() 9local lane = function()
10 local fixture = require "fixture" 10 local fixture = require "fixture"
11 fixture.forever() 11 fixture.sleep_for()
12end 12end
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
21repeat until h1.status == "running" 21repeat 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
24lanes.finally(fixture.throwing_finalizer) 24lanes.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
449void FileRunner::performTest(FileRunnerParam const& testParam_) 461void 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
66enum class [[nodiscard]] TestType 66enum 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
73struct FileRunnerParam 74struct FileRunnerParam