From 022e40cc71beda874d0bad2cec227e472d5dd4af Mon Sep 17 00:00:00 2001 From: Benoit Germain Date: Fri, 18 Apr 2025 10:26:19 +0200 Subject: New feature: Linda periodical cancellation checks * lanes.linda() api change: takes all settings in a single table argument * new linda creation argument wake_period * new lanes.configure setting linda_wake_period * adjusted all unit tests (one TODO test fails on purpose) --- docs/index.html | 46 +- src/lanes.lua | 14 + src/linda.cpp | 152 +++--- src/linda.hpp | 10 +- src/lindafactory.cpp | 9 +- src/universe.cpp | 8 + src/universe.hpp | 2 + tests/basic.lua | 8 +- tests/cancel.lua | 2 +- tests/deadlock.lua | 2 +- tests/error.lua | 4 +- tests/fifo.lua | 4 +- tests/keeper.lua | 36 +- tests/linda_perf.lua | 6 +- tests/tobeclosed.lua | 12 +- unit_tests/embedded_tests.cpp | 4 +- unit_tests/init_and_shutdown.cpp | 533 ++++++++++++--------- unit_tests/linda_tests.cpp | 185 ++++--- unit_tests/scripts/lane/tasking_cancelling.lua | 2 +- .../scripts/lane/tasking_comms_criss_cross.lua | 2 +- unit_tests/scripts/lane/tasking_communications.lua | 2 +- .../scripts/lane/tasking_send_receive_code.lua | 2 +- unit_tests/scripts/linda/multiple_keepers.lua | 6 +- .../scripts/linda/send_registered_userdata.lua | 2 +- 24 files changed, 627 insertions(+), 426 deletions(-) diff --git a/docs/index.html b/docs/index.html index 4dd5848..116fc0a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -64,13 +64,13 @@


- Copyright © 2007-24 Asko Kauppi, Benoit Germain. All rights reserved. + Copyright © 2007-25 Asko Kauppi, Benoit Germain. All rights reserved.
Lua Lanes is published under the same MIT license as Lua 5.1, 5.2, 5.3 and 5.4.

- This document was revised on 17-Mar-25, and applies to version 4.0.0. + This document was revised on 18-Apr-25, and applies to version 4.0.0.

@@ -271,7 +271,7 @@

- lanes.configure accepts an optional options table as sole argument. + lanes.configure accepts an optional table as sole argument. @@ -336,6 +336,22 @@ + + + + + +
name
+ .linda_wake_period + + number > 0 + + Sets the default period in seconds a linda will wake by itself during blocked operations. Default is never.
+ When a Linda enters a blocking call (send(), receive(), receive_batched(), sleep()), it normally sleeps either until the operation completes + or the specified timeout expires. With this setting, the default behavior can be changed to wake periodically. This can help for example with timing issues where a lane is signalled + for cancellation, but a linda inside the lane was in the middle of processing an operation but did not actually start the wait. This can result in the signal to be ignored, thus + causing the Linda to wait out the full operation timeout before cancellation is processed. +
.nb_user_keepers @@ -1222,7 +1238,7 @@
 	local lanes = require "lanes"
 
-	local linda = lanes.linda("my linda")
+	local linda = lanes.linda{name = "my linda"}
 
 	local function loop(max)
 		for i = 1, max do
@@ -1276,16 +1292,24 @@
 

-	h = lanes.linda([name],[group],[close_handler])
+	h = lanes.linda(table|nil)
 

- Arguments to lanes.linda() can be provided in any order, as long as there is a single string, a single number, and a single callable value (all are optional).
- Converting the linda to a string will yield the provided name prefixed by "Linda: ".
- If opt_name is omitted, it will evaluate to an hexadecimal number uniquely representing that linda when the linda is converted to a string. The value is the same as returned by linda:deep().
- If opt_name is "auto", Lanes will try to construct a name from the source location that called lanes.linda(). If that fails, the linda name will be "<unresolved>".
- If Lanes is configured with more than one Keeper state, group is mandatory.
- If the linda is to-be-closed (Lua 5.4+), and a close_handler is provided, it will be called with all the provided arguments. For older Lua versions, its presence will cause an error. + Argument to lanes.linda() is either nil or a single table. The table may contain the following entries: +

    +
  • close_handler: a callable object (function or table/userdata with __call metamethod). If provided, and the linda is to-be-closed (Lua 5.4+), it will be called with all the provided arguments. For older Lua versions, its presence is ignored.
  • +
  • group: an integer between 0 and the number of Keeper states. Mandatory if Lanes is configured with more than one Keeper state. Group 0 is used by the internal timer linda.
  • +
  • + name: a string. Converting the linda to a string will yield the provided name prefixed by "Linda: ". + If omitted or empty, it will evaluate to the string representation of a hexadecimal number uniquely representing that linda when the linda is converted to a string. The numeric value is the same as returned by linda:deep().
    + If "auto", Lanes will try to construct a name from the source location that called lanes.linda(). If that fails, the linda name will be "<unresolved>". +
  • +
  • + wake_period: a number > 0 (unit: seconds). If provided, overrides linda_wake_period provided to lanes.configure(). +
  • +
+ Unknown fields are silently ignored.

diff --git a/src/lanes.lua b/src/lanes.lua
index bd94a14..3ee959c 100644
--- a/src/lanes.lua
+++ b/src/lanes.lua
@@ -96,6 +96,7 @@ local default_params =
     -- it looks also like LuaJIT allocator may not appreciate direct use of its allocator for other purposes than the VM operation
     internal_allocator = isLuaJIT and "libc" or "allocator",
     keepers_gc_threshold = -1,
+    linda_wake_period = 'never',
     nb_user_keepers = 0,
     on_state_create = nil,
     shutdown_timeout = 0.25,
@@ -141,6 +142,19 @@ local param_checkers =
         end
         return true
     end,
+    linda_wake_period = function(val_)
+        -- linda_wake_period should be a number > 0, or the string 'never'
+        if val_ == 'never' then
+            return true
+        end
+        if type(val_) ~= "number" then
+            return nil, "not a number"
+        end
+        if val_ <= 0 then
+            return nil, "value out of range"
+        end
+        return true
+    end,
     nb_user_keepers = function(val_)
         -- nb_user_keepers should be a number in [0,100] (so that nobody tries to run OOM by specifying a huge amount)
         if type(val_) ~= "number" then
diff --git a/src/linda.cpp b/src/linda.cpp
index 0cdacfa..a9ae61c 100644
--- a/src/linda.cpp
+++ b/src/linda.cpp
@@ -237,11 +237,23 @@ namespace {
                     _lane->waiting_on = &_linda->writeHappened;
                     _lane->status.store(Lane::Waiting, std::memory_order_release);
                 }
+
+                // wait until the final target date by small increments, interrupting regularly so that we can check for cancel requests,
+                // in case some timing issue caused a cancel request to be issued, and the condvar signalled, before we actually wait for it
+                auto const [_forceTryAgain, _until_check_cancel] = std::invoke([_until, wakePeriod = _linda->getWakePeriod()] {
+                    auto _until_check_cancel{ std::chrono::time_point::max() };
+                    if (wakePeriod.count() > 0.0f) {
+                        _until_check_cancel = std::chrono::steady_clock::now() + std::chrono::duration_cast(wakePeriod);
+                    }
+                    bool const _forceTryAgain{ _until_check_cancel < _until };
+                    return std::make_tuple(_forceTryAgain, _forceTryAgain ? _until_check_cancel : _until);
+                });
+
                 // not enough data to read: wakeup when data was sent, or when timeout is reached
                 std::unique_lock _guard{ _keeper->mutex, std::adopt_lock };
-                std::cv_status const _status{ _linda->writeHappened.wait_until(_guard, _until) };
+                std::cv_status const _status{ _linda->writeHappened.wait_until(_guard, _until_check_cancel) };
                 _guard.release(); // we don't want to unlock the mutex on exit!
-                _try_again = (_status == std::cv_status::no_timeout); // detect spurious wakeups
+                _try_again = _forceTryAgain || (_status == std::cv_status::no_timeout); // detect spurious wakeups
                 if (_lane != nullptr) {
                     _lane->waiting_on = nullptr;
                     _lane->status.store(_prev_status, std::memory_order_release);
@@ -296,9 +308,10 @@ LUAG_FUNC(linda);
 // #################################################################################################
 // #################################################################################################
 
-Linda::Linda(Universe* const U_, LindaGroup const group_, std::string_view const& name_)
+Linda::Linda(Universe* const U_, std::string_view const& name_, lua_Duration const wake_period_, LindaGroup const group_)
 : DeepPrelude{ LindaFactory::Instance }
 , U{ U_ }
+, wakePeriod{ wake_period_ }
 , keeperIndex{ group_ % U_->keepers.getNbKeepers() }
 {
     setName(name_);
@@ -330,9 +343,13 @@ Linda* Linda::CreateTimerLinda(lua_State* const L_)
     STACK_CHECK_START_REL(L_, 0);                                                                  // L_:
     // Initialize 'timerLinda'; a common Linda object shared by all states
     lua_pushcfunction(L_, LG_linda);                                                               // L_: lanes.linda
-    luaG_pushstring(L_, "lanes-timer");                                                            // L_: lanes.linda "lanes-timer"
-    lua_pushinteger(L_, 0);                                                                        // L_: lanes.linda "lanes-timer" 0
-    lua_call(L_, 2, 1);                                                                            // L_: linda
+    lua_createtable(L_, 0, 3);                                                                     // L_: lanes.linda {}
+    luaG_pushstring(L_, "lanes-timer");                                                            // L_: lanes.linda {} "lanes-timer"
+    luaG_setfield(L_, StackIndex{ -2 }, std::string_view{ "name" });                               // L_: lanes.linda { .name="lanes-timer" }
+    lua_pushinteger(L_, 0);                                                                        // L_: lanes.linda { .name="lanes-timer" } 0
+    luaG_setfield(L_, StackIndex{ -2 }, std::string_view{ "group" });                              // L_: lanes.linda { .name="lanes-timer" .group = 0 }
+    // note that wake_period is not set (will default to the value in the universe)
+    lua_call(L_, 1, 1);                                                                            // L_: linda
     STACK_CHECK(L_, 1);
 
     // Proxy userdata contents is only a 'DeepPrelude*' pointer
@@ -941,11 +958,23 @@ LUAG_FUNC(linda_send)
                         _lane->waiting_on = &_linda->readHappened;
                         _lane->status.store(Lane::Waiting, std::memory_order_release);
                     }
+
+                    // wait until the final target date by small increments, interrupting regularly so that we can check for cancel requests,
+                    // in case some timing issue caused a cancel request to be issued, and the condvar signalled, before we actually wait for it
+                    auto const [_forceTryAgain, _until_check_cancel] = std::invoke([_until, wakePeriod = _linda->getWakePeriod()] {
+                        auto _until_check_cancel{ std::chrono::time_point::max() };
+                        if (wakePeriod.count() > 0.0f) {
+                            _until_check_cancel = std::chrono::steady_clock::now() + std::chrono::duration_cast(wakePeriod);
+                        }
+                        bool const _forceTryAgain{ _until_check_cancel < _until };
+                        return std::make_tuple(_forceTryAgain, _forceTryAgain ? _until_check_cancel : _until);
+                    });
+
                     // could not send because no room: wait until some data was read before trying again, or until timeout is reached
                     std::unique_lock _guard{ _keeper->mutex, std::adopt_lock };
-                    std::cv_status const status{ _linda->readHappened.wait_until(_guard, _until) };
+                    std::cv_status const status{ _linda->readHappened.wait_until(_guard, _until_check_cancel) };
                     _guard.release(); // we don't want to unlock the mutex on exit!
-                    _try_again = (status == std::cv_status::no_timeout); // detect spurious wakeups
+                    _try_again = _forceTryAgain || (status == std::cv_status::no_timeout); // detect spurious wakeups
                     if (_lane != nullptr) {
                         _lane->waiting_on = nullptr;
                         _lane->status.store(_prev_status, std::memory_order_release);
@@ -1129,88 +1158,69 @@ namespace {
 // #################################################################################################
 
 /*
- * ud = lanes.linda( [name[,group[,close_handler]]])
+ * ud = lanes.linda{.name = , .group = , .close_handler = , .wake_period = }
  *
  * returns a linda object, or raises an error if creation failed
  */
 LUAG_FUNC(linda)
 {
-    static constexpr StackIndex kLastArg{ LUA_VERSION_NUM >= 504 ? 3 : 2 };
+    // unpack the received table on the stack, putting name wake_period group close_handler in that order
     StackIndex const _top{ lua_gettop(L_) };
-    luaL_argcheck(L_, _top <= kLastArg, _top, "too many arguments");
-    StackIndex _closeHandlerIdx{};
-    StackIndex _nameIdx{};
-    StackIndex _groupIdx{};
-    for (StackIndex const _i : std::ranges::iota_view{ StackIndex{ 1 }, StackIndex{ _top + 1 }}) {
-        switch (luaG_type(L_, _i)) {
+    luaL_argcheck(L_, _top <= 1, _top, "too many arguments");
+    if (_top == 0) {
+        lua_settop(L_, 3);                                                                         // L_: nil nil nil
+    }
+    else if (!lua_istable(L_, kIdxTop)) {
+        luaL_argerror(L_, 1, "expecting a table");
+    } else {
+        auto* const _U{ Universe::Get(L_) };
+        lua_getfield(L_, 1, "wake_period");                                                        // L_: {} wake_period
+        if (lua_isnil(L_, kIdxTop)) {
+            lua_pop(L_, 1);
+            lua_pushnumber(L_, _U->lindaWakePeriod.count());
+        } else {
+            luaL_argcheck(L_, luaL_optnumber(L_, 2, 0) > 0, 1, "wake_period must be > 0");
+        }
+
+        lua_getfield(L_, 1, "group");                                                              // L_: {} wake_period group
+        int const _nbKeepers{ _U->keepers.getNbKeepers() };
+        if (lua_isnil(L_, kIdxTop)) {
+            luaL_argcheck(L_, _nbKeepers < 2, 0, "Group is mandatory in multiple Keeper scenarios");
+        } else {
+            int const _group{ static_cast(lua_tointeger(L_, kIdxTop)) };
+            luaL_argcheck(L_, _group >= 0 && _group < _nbKeepers, 1, "group out of range");
+        }
+
 #if LUA_VERSION_NUM >= 504 // to-be-closed support starts with Lua 5.4
-        case LuaType::FUNCTION:
-            luaL_argcheck(L_, _closeHandlerIdx == 0, _i, "More than one __close handler");
-            _closeHandlerIdx = _i;
-            break;
-
-        case LuaType::USERDATA:
-        case LuaType::TABLE:
-            luaL_argcheck(L_, _closeHandlerIdx == 0, _i, "More than one __close handler");
-            luaL_argcheck(L_, luaL_getmetafield(L_, _i, "__call") != 0, _i, "__close handler is not callable");
+        lua_getfield(L_, 1, "close_handler");                                                      // L_: {} wake_period group close_handler
+        LuaType const _handlerType{ luaG_type(L_, kIdxTop) };
+        if (_handlerType == LuaType::NIL) {
+            lua_pop(L_, 1);                                                                        // L_: {} wake_period group
+        } else if (_handlerType == LuaType::USERDATA || _handlerType == LuaType::TABLE) {
+            luaL_argcheck(L_, luaL_getmetafield(L_, kIdxTop, "__call") != 0, 1, "__close handler is not callable");
             lua_pop(L_, 1); // luaL_getmetafield() pushed the field, we need to pop it
-            _closeHandlerIdx = _i;
-            break;
+        } else {
+            luaL_argcheck(L_, _handlerType == LuaType::FUNCTION, 1, "__close handler is not a function");
+        }
 #endif // LUA_VERSION_NUM >= 504
 
-        case LuaType::STRING:
-            luaL_argcheck(L_, _nameIdx == 0, _i, "More than one name");
-            _nameIdx = _i;
-            break;
-
-        case LuaType::NUMBER:
-            luaL_argcheck(L_, _groupIdx == 0, _i, "More than one group");
-            _groupIdx = _i;
-            break;
-
-        default:
-            luaL_argcheck(L_, false, _i, "Bad argument type (should be a string, a number, or a callable type)");
-        }
-    }
- 
-    int const _nbKeepers{ Universe::Get(L_)->keepers.getNbKeepers() };
-    if (!_groupIdx) {
-        luaL_argcheck(L_, _nbKeepers < 2, 0, "Group is mandatory in multiple Keeper scenarios");
-    } else {
-        int const _group{ static_cast(lua_tointeger(L_, _groupIdx)) };
-        luaL_argcheck(L_, _group >= 0 && _group < _nbKeepers, _groupIdx, "Group out of range");
+        auto const _nameType{ luaG_getfield(L_, StackIndex{ 1 }, "name") };                        // L_: {} wake_period group [close_handler] name
+        luaL_argcheck(L_, _nameType == LuaType::NIL || _nameType == LuaType::STRING, 1, "name is not a string");
+        lua_replace(L_, 1);                                                                        // L_: name wake_period group [close_handler]
     }
 
     // done with argument checking, let's proceed
-    if constexpr (LUA_VERSION_NUM >= 504) {
-        // make sure we have kMaxArgs arguments on the stack for processing, with name, group, and handler, in that order
-        lua_settop(L_, kLastArg);                                                                  // L_: a b c
-        // If either index is 0, lua_settop() adjusted the stack with a nil in slot kLastArg
-        lua_pushvalue(L_, _closeHandlerIdx ? _closeHandlerIdx : kLastArg);                         // L_: a b c close_handler
-        lua_pushvalue(L_, _groupIdx ? _groupIdx : kLastArg);                                       // L_: a b c close_handler group
-        lua_pushvalue(L_, _nameIdx ? _nameIdx : kLastArg);                                         // L_: a b c close_handler group name
-        lua_replace(L_, 1);                                                                        // L_: name b c close_handler group
-        lua_replace(L_, 2);                                                                        // L_: name group c close_handler
-        lua_replace(L_, 3);                                                                        // L_: name group close_handler
-
+    if (lua_gettop(L_) == 4) {
         // if we have a __close handler, we need a uservalue slot to store it
-        UserValueCount const _nuv{ _closeHandlerIdx ? 1 : 0 };
-        LindaFactory::Instance.pushDeepUserdata(DestState{ L_ }, _nuv);                            // L_: name group close_handler linda
-        if (_closeHandlerIdx != 0) {
-            lua_replace(L_, 2);                                                                    // L_: name linda close_handler
-            lua_setiuservalue(L_, StackIndex{ 2 }, UserValueIndex{ 1 });                           // L_: name linda
-        }
+        LindaFactory::Instance.pushDeepUserdata(DestState{ L_ }, UserValueCount{ 1 });             // L_: name wake_period group [close_handler] linda
+        lua_replace(L_, 3);                                                                        // L_: name wake_period linda close_handler
+        lua_setiuservalue(L_, StackIndex{ 3 }, UserValueIndex{ 1 });                               // L_: name wake_period linda
         // depending on whether we have a handler or not, the stack is not in the same state at this point
         // just make sure we have our Linda at the top
         LUA_ASSERT(L_, ToLinda(L_, kIdxTop));
         return 1;
     } else { // no to-be-closed support
-        // ensure we have name, group in that order on the stack
-        if (_nameIdx > _groupIdx) {
-            lua_insert(L_, 1);                                                                     // L_: name group
-        }
-        LindaFactory::Instance.pushDeepUserdata(DestState{ L_ }, UserValueCount{ 0 });             // L_: name group linda
+        LindaFactory::Instance.pushDeepUserdata(DestState{ L_ }, UserValueCount{ 0 });             // L_: name wake_period group linda
         return 1;
     }
-
 }
diff --git a/src/linda.hpp b/src/linda.hpp
index 2d5c9dc..7874db3 100644
--- a/src/linda.hpp
+++ b/src/linda.hpp
@@ -42,19 +42,21 @@ class Linda final
     };
     using enum Status;
 
-    private:
+    public:
+    Universe* const U{ nullptr }; // the universe this linda belongs to
 
+    private:
     static constexpr size_t kEmbeddedNameLength = 24;
     using EmbeddedName = std::array;
     // depending on the name length, it is either embedded inside the Linda, or allocated separately
     std::variant nameVariant{};
     // counts the keeper operations in progress
     std::atomic keeperOperationCount{};
+    lua_Duration wakePeriod{};
 
     public:
     std::condition_variable readHappened{};
     std::condition_variable writeHappened{};
-    Universe* const U{ nullptr }; // the universe this linda belongs to
     KeeperIndex const keeperIndex{ -1 }; // the keeper associated to this linda
     Status cancelStatus{ Status::Active };
 
@@ -68,7 +70,7 @@ class Linda final
     static void operator delete(void* p_) { static_cast(p_)->U->internalAllocator.free(p_, sizeof(Linda)); }
 
     ~Linda();
-    Linda(Universe* U_, LindaGroup group_, std::string_view const& name_);
+    Linda(Universe* U_, std::string_view const& name_, lua_Duration wake_period_, LindaGroup group_);
     Linda() = delete;
     // non-copyable, non-movable
     Linda(Linda const&) = delete;
@@ -92,6 +94,8 @@ class Linda final
     [[nodiscard]]
     std::string_view getName() const;
     [[nodiscard]]
+    auto getWakePeriod() const { return wakePeriod; }
+    [[nodiscard]]
     bool inKeeperOperation() const { return keeperOperationCount.load(std::memory_order_seq_cst) != 0; }
     template 
     [[nodiscard]]
diff --git a/src/lindafactory.cpp b/src/lindafactory.cpp
index 42d0984..4eab0c1 100644
--- a/src/lindafactory.cpp
+++ b/src/lindafactory.cpp
@@ -108,9 +108,11 @@ std::string_view LindaFactory::moduleName() const
 
 DeepPrelude* LindaFactory::newDeepObjectInternal(lua_State* const L_) const
 {
-    // we always expect name and group at the bottom of the stack (either can be nil). any extra stuff we ignore and keep unmodified
+    STACK_CHECK_START_REL(L_, 0);
+    // we always expect name, wake_period, group at the bottom of the stack (either can be nil). any extra stuff we ignore and keep unmodified
     std::string_view _linda_name{ luaG_tostring(L_, StackIndex{ 1 }) };
-    LindaGroup _linda_group{ static_cast(lua_tointeger(L_, 2)) };
+    auto const _wake_period{ static_cast(lua_tonumber(L_, 2)) };
+    LindaGroup const _linda_group{ static_cast(lua_tointeger(L_, 3)) };
 
     // store in the linda the location of the script that created it
     if (_linda_name == "auto") {
@@ -129,6 +131,7 @@ DeepPrelude* LindaFactory::newDeepObjectInternal(lua_State* const L_) const
     // The deep data is allocated separately of Lua stack; we might no longer be around when last reference to it is being released.
     // One can use any memory allocation scheme. Just don't use L's allocF because we don't know which state will get the honor of GCing the linda
     Universe* const _U{ Universe::Get(L_) };
-    Linda* const _linda{ new (_U) Linda{ _U, _linda_group, _linda_name } };
+    Linda* const _linda{ new (_U) Linda{ _U, _linda_name, _wake_period, _linda_group } };
+    STACK_CHECK(L_, 0);
     return _linda;
 }
diff --git a/src/universe.cpp b/src/universe.cpp
index 89ad02a..335f056 100644
--- a/src/universe.cpp
+++ b/src/universe.cpp
@@ -153,6 +153,14 @@ Universe* Universe::Create(lua_State* const L_)
     lua_setmetatable(L_, -2);                                                                      // L_: settings universe
     lua_pop(L_, 1);                                                                                // L_: settings
 
+    std::ignore = luaG_getfield(L_, kIdxSettings, "linda_wake_period");                            // L_: settings linda_wake_period
+    if (luaG_type(L_, kIdxTop) == LuaType::NUMBER) {
+        _U->lindaWakePeriod = lua_Duration{ lua_tonumber(L_, kIdxTop) };
+    } else {
+        LUA_ASSERT(L_, luaG_tostring(L_, kIdxTop) == "never");
+    }
+    lua_pop(L_, 1);                                                                                // L_: settings
+
     std::ignore = luaG_getfield(L_, kIdxSettings, "strip_functions");                              // L_: settings strip_functions
     _U->stripFunctions = lua_toboolean(L_, -1) ? true : false;
     lua_pop(L_, 1);                                                                                // L_: settings
diff --git a/src/universe.hpp b/src/universe.hpp
index 42a3d83..0c5e659 100644
--- a/src/universe.hpp
+++ b/src/universe.hpp
@@ -99,6 +99,8 @@ class Universe final
 
     Keepers keepers;
 
+    lua_Duration lindaWakePeriod{};
+
     // Initialized by 'init_once_LOCKED()': the deep userdata Linda object
     // used for timers (each lane will get a proxy to this)
     Linda* timerLinda{ nullptr };
diff --git a/tests/basic.lua b/tests/basic.lua
index a9c85cc..9aaad97 100644
--- a/tests/basic.lua
+++ b/tests/basic.lua
@@ -163,7 +163,7 @@ PRINT(" "..st)
 assert(st == "cancelled", "st is '" .. st .. "' instead of 'cancelled'")
 
 -- cancellation of lanes waiting on a linda
-local limited = lanes_linda("limited")
+local limited = lanes_linda{name = "limited"}
 assert.fails(function() limited:limit("key", -1) end)
 assert.failsnot(function() limited:limit("key", 1) end)
 -- [[################################################
@@ -255,7 +255,7 @@ local chunk= function(linda)
     WR("chunk ", "Lane ends!\n")
 end
 
-local linda = lanes_linda("communications")
+local linda = lanes_linda{name = "communications"}
 assert(type(linda) == "userdata" and tostring(linda) == "Linda: communications")
     --
     -- ["->"] master -> slave
@@ -410,7 +410,7 @@ local tc = lanes.gen("io", { name = 'auto', gc_cb = gc_cb },
   end
 )
 
-local linda= lanes_linda("criss cross")
+local linda= lanes_linda{name = "criss cross"}
 
 local a,b= tc(linda, "A","B"), tc(linda, "B","A")   -- launching two lanes, twisted comms
 
@@ -461,7 +461,7 @@ local function chunk2(linda)
     linda:send("up", function() return ":)" end, "ok2")
 end
 
-local linda = lanes_linda("auto")
+local linda = lanes_linda{name = "auto"}
 local t2 = lanes.gen("debug,string,io", { name = 'auto', gc_cb = gc_cb }, chunk2)(linda)     -- prepare & launch
 linda:send("down", function(linda) linda:send("up", "ready!") end,
                     "ok")
diff --git a/tests/cancel.lua b/tests/cancel.lua
index 80e6c6a..66957c3 100644
--- a/tests/cancel.lua
+++ b/tests/cancel.lua
@@ -148,7 +148,7 @@ local protectedBody = function(...)
 	local paramLessClosure = function() laneBody(unpack(params)) end
 	local status, message = xpcall(paramLessClosure, errorHandler)
 	if status == false then
-		print("			error handler rethrowing '" .. (ce == message and "cancel_error"or tostring(message)) .. "'")
+		print("			protectedBody rethrowing '" .. (ce == message and "cancel_error" or tostring(message)) .. "'")
 		-- if the error isn't rethrown, the lane's finalizer won't get it
 		error(message)
 	end
diff --git a/tests/deadlock.lua b/tests/deadlock.lua
index d028e83..9b93e3b 100644
--- a/tests/deadlock.lua
+++ b/tests/deadlock.lua
@@ -16,7 +16,7 @@ print "let's begin"
 local do_extra_stuff = true
 
 if do_extra_stuff then
-    local linda = lanes.linda "deadlock_linda"
+    local linda = lanes.linda{name = "deadlock_linda"}
     -- just something to make send() succeed and receive() fail
     local payload = { io.flush }
 
diff --git a/tests/error.lua b/tests/error.lua
index 306c51d..28cfff1 100644
--- a/tests/error.lua
+++ b/tests/error.lua
@@ -106,11 +106,11 @@ end
 
 local lane_error_as_string = "'lane error as string'"
 local lane_error_as_table = setmetatable({"lane error as table"}, make_table_error_mt())
-local lane_error_as_linda = lanes.linda("'lane error'")
+local lane_error_as_linda = lanes.linda{name = "'lane error'"}
 
 local finalizer_error_as_string = "'finalizer error as string'"
 local finalizer_error_as_table = setmetatable({"finalizer error as table"}, make_table_error_mt())
-local finalizer_error_as_linda = lanes.linda("'finalizer error'")
+local finalizer_error_as_linda = lanes.linda{name = "'finalizer error'"}
 
 local test_settings = {}
 local configure_tests = function()
diff --git a/tests/fifo.lua b/tests/fifo.lua
index 9efcbd9..1317a9f 100644
--- a/tests/fifo.lua
+++ b/tests/fifo.lua
@@ -6,10 +6,10 @@
 
 local lanes = require "lanes".configure{shutdown_timeout=3,with_timers=true}
 
-local atomic_linda = lanes.linda( "atom")
+local atomic_linda = lanes.linda{name = "atom"}
 local atomic_inc= lanes.genatomic( atomic_linda, "FIFO_n")
 
-local fifo_linda = lanes.linda( "fifo")
+local fifo_linda = lanes.linda{name = "fifo"}
 
 -- Lua 5.1 support
 local table_unpack = table.unpack or unpack
diff --git a/tests/keeper.lua b/tests/keeper.lua
index f566927..4742732 100644
--- a/tests/keeper.lua
+++ b/tests/keeper.lua
@@ -40,14 +40,14 @@ if true then
     end
 
     -- should succeed
-    assert.failsnot(function() createLinda("zero", 0) end)
-    assert.failsnot(function() createLinda("one", 1) end)
-    assert.failsnot(function() createLinda("two", 2) end)
-    assert.failsnot(function() createLinda("three", 3) end)
+    assert.failsnot(function() createLinda{name = "zero", group = 0} end)
+    assert.failsnot(function() createLinda{name = "one", group = 1} end)
+    assert.failsnot(function() createLinda{name = "two", group = 2} end)
+    assert.failsnot(function() createLinda{name = "three", group = 3} end)
     -- should fail (and not create the lindas)
-    assert.fails(function() createLinda("minus 1", -1) end)
-    assert.fails(function() createLinda("none") end)
-    assert.fails(function() createLinda("four", 4) end)
+    assert.fails(function() createLinda{name = "minus 1", group = -1} end)
+    assert.fails(function() createLinda{name = "none"} end)
+    assert.fails(function() createLinda{name = "four", group = 4} end)
 
 end
 -- should only collect the 4 successfully created lindas
@@ -58,11 +58,11 @@ DONE()
 if true then
     PRINT "========================================================================================="
     PRINT "Linda names test:"
-    local unnamedLinda1 = lanes.linda(1)
-    local unnamedLinda2 = lanes.linda("", 2)
-    local veeeerrrryyyylooongNamedLinda3 = lanes.linda( "veeeerrrryyyylooongNamedLinda", 3)
+    local unnamedLinda1 = lanes.linda{group = 1}
+    local unnamedLinda2 = lanes.linda{name = "", group = 2}
+    local veeeerrrryyyylooongNamedLinda3 = lanes.linda{ name = "veeeerrrryyyylooongNamedLinda", group = 3}
     assert(tostring(veeeerrrryyyylooongNamedLinda3) == "Linda: veeeerrrryyyylooongNamedLinda")
-    local shortNamedLinda0 = lanes.linda( "short", 0)
+    local shortNamedLinda0 = lanes.linda{name = "short", group = 0}
     assert(tostring(shortNamedLinda0) == "Linda: short")
     PRINT(shortNamedLinda0, unnamedLinda1, unnamedLinda2, veeeerrrryyyylooongNamedLinda3)
 end
@@ -74,12 +74,12 @@ DONE()
 if true then
     PRINT "========================================================================================="
     PRINT "Linda GC test:"
-    local a = lanes.linda("A", 1)
-    local b = lanes.linda("B", 2)
-    local c = lanes.linda("C", 3)
+    local a = lanes.linda{name = "A", group = 1}
+    local b = lanes.linda{name = "B", group = 2}
+    local c = lanes.linda{name = "C", group = 3}
 
     -- store lindas in each other and in themselves
-    a:set("here", lanes.linda("temporary linda", 0))
+    a:set("here", lanes.linda{name = "temporary linda", group = 0})
     b:set("here", a, b, c)
     c:set("here", a, b, c)
 
@@ -120,13 +120,13 @@ if true then
     end
 
     --
-    local lindaA= lanes.linda( "A", 1)
+    local lindaA= lanes.linda{name = "A", group = 1}
     local A= keeper( lindaA )
 
-    local lindaB= lanes.linda( "B", 2)
+    local lindaB= lanes.linda{name = "B", group = 2}
     local B= keeper( lindaB )
 
-    local lindaC= lanes.linda( "C", 3)
+    local lindaC= lanes.linda{name = "C", group = 3}
     local C= keeper( lindaC )
     PRINT("Created", lindaA, lindaB, lindaC)
 
diff --git a/tests/linda_perf.lua b/tests/linda_perf.lua
index 107fd25..83b8921 100644
--- a/tests/linda_perf.lua
+++ b/tests/linda_perf.lua
@@ -22,7 +22,7 @@ if true then
 	do
 		print "############################################ tests get/set"
 		-- linda:get throughput
-		local l = lanes.linda("get/set", 1)
+		local l = lanes.linda{name = "get/set", group = 1}
 		local batch = {}
 		for i = 1,1000 do
 			table.insert(batch, i)
@@ -90,7 +90,7 @@ local group_uid = 1
 local function ziva1( preloop, loop, batch)
 	-- prefill the linda a bit to increase fifo stress
 	local top = math.max( preloop, loop)
-	local l = lanes.linda("ziva1("..preloop..":"..loop..":"..batch..")", group_uid)
+	local l = lanes.linda{name = "ziva1("..preloop..":"..loop..":"..batch..")", group = group_uid}
 	group_uid = (group_uid % config.nb_user_keepers) + 1
 	local t1 = lanes.now_secs()
 	for i = 1, preloop do
@@ -165,7 +165,7 @@ end
 
 -- sequential write/read (no parallelization involved)
 local function ziva2( preloop, loop, batch)
-	local l = lanes.linda("ziva2("..preloop..":"..loop..":"..tostring(batch)..")", group_uid)
+	local l = lanes.linda{name = "ziva2("..preloop..":"..loop..":"..tostring(batch)..")", group = group_uid}
 	group_uid = (group_uid % config.nb_user_keepers) + 1
 	-- prefill the linda a bit to increase fifo stress
 	local top, step = math.max( preloop, loop), batch or 1
diff --git a/tests/tobeclosed.lua b/tests/tobeclosed.lua
index ef09df3..447b936 100644
--- a/tests/tobeclosed.lua
+++ b/tests/tobeclosed.lua
@@ -36,7 +36,7 @@ do
             WR("f closing ", linda_)
             closed_by_f = true
         end
-        local lf  = lanes.linda("closed by f", close_handler_f)
+        local lf  = lanes.linda{name = "closed by f", close_handler = close_handler_f}
 
         local close_handler_t = setmetatable({},
             {
@@ -46,7 +46,7 @@ do
                 end
             }
         )
-        local lt  = lanes.linda("closed by t", close_handler_t)
+        local lt  = lanes.linda{name = "closed by t", close_handler = close_handler_t}
     end
     assert(closed_by_f == true)
     assert(closed_by_t == true)
@@ -58,13 +58,13 @@ end
 WR "================================================================================================"
 WR "Through Linda"
 do
-    local l = lanes.linda("channel")
+    local l = lanes.linda{name = "channel"}
 
     local close_handler_f = function(linda_, err_)
         WR("f closing ", linda_)
         linda_:set("closed", true)
     end
-    local l_in = lanes.linda("voyager", close_handler_f)
+    local l_in = lanes.linda{name = "voyager", close_handler = close_handler_f}
     l:set("trip", l_in)
 
     do
@@ -99,7 +99,7 @@ end
 WR "================================================================================================"
 WR "Linda closing through Lane"
 do
-    local l = lanes.linda("channel")
+    local l = lanes.linda{name = "channel"}
     local lane_body = function(l_arg_)
         WR "In lane body"
         -- linda obtained through a linda
@@ -114,7 +114,7 @@ do
         local _count, _closed = linda_:get("closed")
         linda_:set("closed", (_closed or 0) + 1)
     end
-    local l_in = lanes.linda("voyager", close_handler_f)
+    local l_in = lanes.linda{name = "voyager", close_handler = close_handler_f}
     l:set("trip", l_in)
 
     do
diff --git a/unit_tests/embedded_tests.cpp b/unit_tests/embedded_tests.cpp
index 0991a4c..a0a7bb2 100644
--- a/unit_tests/embedded_tests.cpp
+++ b/unit_tests/embedded_tests.cpp
@@ -157,7 +157,7 @@ TEST_CASE("lanes.embedding.with default allocator")
         // function with an upvalue
         std::string_view const _script{
             " local lanes = require 'lanes'.configure{with_timers = false}"
-            " local l = lanes.linda'gleh'"
+            " local l = lanes.linda{name = 'gleh'}"
             " local upvalue = 'oeauaoeuoeuaoeuaoeujaoefubycfjbycfybcfjybcfjybcfjbcf'"
             " local upvalued = function()"
             "     return upvalue"
@@ -183,7 +183,7 @@ TEST_CASE("lanes.embedding.with default allocator")
         // try to send io.open into a linda, which fails if io base library is not loaded
         std::string_view const _script{
             " local lanes = require 'lanes'"
-            " local l = lanes.linda'gleh'"
+            " local l = lanes.linda{name = 'gleh'}"
             " l:set('yo', io.open)"
             " return 'SUCCESS'"
         };
diff --git a/unit_tests/init_and_shutdown.cpp b/unit_tests/init_and_shutdown.cpp
index 384af43..bd72157 100644
--- a/unit_tests/init_and_shutdown.cpp
+++ b/unit_tests/init_and_shutdown.cpp
@@ -289,6 +289,62 @@ TEST_CASE("lanes.configure.keepers_gc_threshold")
 
 // #################################################################################################
 
+TEST_CASE("lanes.configure.linda_wake_period")
+{
+    LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
+
+    // linda_wake_period should be a number > 0, or 'never'
+
+    SECTION("linda_wake_period = ")
+    {
+        L.requireFailure("require 'lanes'.configure{linda_wake_period = {}}");
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
+    SECTION("linda_wake_period = ")
+    {
+        L.requireFailure("require 'lanes'.configure{linda_wake_period = 'gluh'}");
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
+    SECTION("linda_wake_period = 'never'")
+    {
+        L.requireSuccess("require 'lanes'.configure{linda_wake_period = 'never'}");
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
+    SECTION("linda_wake_period = ")
+    {
+        L.requireFailure("require 'lanes'.configure{linda_wake_period = -0.001}");
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
+    SECTION("linda_wake_period = 0")
+    {
+        L.requireFailure("require 'lanes'.configure{linda_wake_period = 0}");
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
+    SECTION("linda_wake_period = 0.0001s")
+    {
+        L.requireSuccess("require 'lanes'.configure{linda_wake_period = 0.0001}");
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
+    SECTION("linda_wake_period = 1e30")
+    {
+        L.requireSuccess("require 'lanes'.configure{linda_wake_period = 1e30}");
+    }
+}
+
+// #################################################################################################
+
 TEST_CASE("lanes.configure.nb_user_keepers")
 {
     LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
@@ -300,35 +356,35 @@ TEST_CASE("lanes.configure.nb_user_keepers")
         L.requireFailure("require 'lanes'.configure{nb_user_keepers = {}}");
     }
 
-    // -----------------------------------------------------------------------------------------
+    // ---------------------------------------------------------------------------------------------
 
     SECTION("nb_user_keepers = ")
     {
         L.requireFailure("require 'lanes'.configure{nb_user_keepers = 'gluh'}");
     }
 
-    // -----------------------------------------------------------------------------------------
+    // ---------------------------------------------------------------------------------------------
 
     SECTION("nb_user_keepers = -1")
     {
         L.requireFailure("require 'lanes'.configure{nb_user_keepers = -1}");
     }
 
-    // -----------------------------------------------------------------------------------------
+    // ---------------------------------------------------------------------------------------------
 
     SECTION("nb_user_keepers = 0")
     {
         L.requireSuccess("require 'lanes'.configure{nb_user_keepers = 0}");
     }
 
-    // -----------------------------------------------------------------------------------------
+    // ---------------------------------------------------------------------------------------------
 
     SECTION("nb_user_keepers = 1")
     {
         L.requireSuccess("require 'lanes'.configure{nb_user_keepers = 1}");
     }
 
-    // -----------------------------------------------------------------------------------------
+    // ---------------------------------------------------------------------------------------------
 
     SECTION("nb_user_keepers = 100")
     {
@@ -345,340 +401,355 @@ TEST_CASE("lanes.configure.nb_user_keepers")
 
 // #################################################################################################
 
-TEST_CASE("lanes.configure.the rest")
+TEST_CASE("lanes.configure.on_state_create")
 {
     LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
 
     // on_state_create should be a function, either C or Lua, without upvalues
 
-    SECTION("on_state_create")
+    SECTION("on_state_create = 
") { - SECTION("on_state_create =
") - { - L.requireFailure("require 'lanes'.configure{on_state_create = {}}"); - } + L.requireFailure("require 'lanes'.configure{on_state_create = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("on_state_create = ") - { - L.requireFailure("require 'lanes'.configure{on_state_create = 'gluh'}"); - } + SECTION("on_state_create = ") + { + L.requireFailure("require 'lanes'.configure{on_state_create = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("on_state_create = ") - { - L.requireFailure("require 'lanes'.configure{on_state_create = 1}"); - } + SECTION("on_state_create = ") + { + L.requireFailure("require 'lanes'.configure{on_state_create = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("on_state_create = false") - { - L.requireFailure("require 'lanes'.configure{on_state_create = false}"); - } + SECTION("on_state_create = false") + { + L.requireFailure("require 'lanes'.configure{on_state_create = false}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("on_state_create = true") - { - L.requireFailure("require 'lanes'.configure{on_state_create = true}"); - } + SECTION("on_state_create = true") + { + L.requireFailure("require 'lanes'.configure{on_state_create = true}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("on_state_create = ") - { - // on_state_create isn't called inside a Keeper state if it's a Lua function (which is good as print() doesn't exist there!) - L.requireSuccess("local print = print; require 'lanes'.configure{on_state_create = function() print 'hello' end}"); - } + SECTION("on_state_create = ") + { + // on_state_create isn't called inside a Keeper state if it's a Lua function (which is good as print() doesn't exist there!) + L.requireSuccess("local print = print; require 'lanes'.configure{on_state_create = function() print 'hello' end}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("on_state_create = ") - { - // funnily enough, in Lua 5.3, print() uses global tostring(), that doesn't exist in a keeper since we didn't open libs -> "attempt to call a nil value" - // conclusion, don't use print() as a fake on_state_create() callback! - // assert() should be fine since we pass a non-false argument to on_state_create - L.requireSuccess("require 'lanes'.configure{on_state_create = assert}"); - } + SECTION("on_state_create = ") + { + // funnily enough, in Lua 5.3, print() uses global tostring(), that doesn't exist in a keeper since we didn't open libs -> "attempt to call a nil value" + // conclusion, don't use print() as a fake on_state_create() callback! + // assert() should be fine since we pass a non-false argument to on_state_create + L.requireSuccess("require 'lanes'.configure{on_state_create = assert}"); } +} + +// ################################################################################################# + +TEST_CASE("lanes.configure.shutdown_timeout") +{ + LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; - // --------------------------------------------------------------------------------------------- // shutdown_timeout should be a number in [0,3600] - SECTION("shutdown_timeout") + SECTION("shutdown_timeout =
") { - SECTION("shutdown_timeout =
") - { - L.requireFailure("require 'lanes'.configure{shutdown_timeout = {}}"); - } + L.requireFailure("require 'lanes'.configure{shutdown_timeout = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("shutdown_timeout = ") - { - L.requireFailure("require 'lanes'.configure{shutdown_timeout = 'gluh'}"); - } + SECTION("shutdown_timeout = ") + { + L.requireFailure("require 'lanes'.configure{shutdown_timeout = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("shutdown_timeout = ") - { - L.requireFailure("require 'lanes'.configure{shutdown_timeout = -0.001}"); - } + SECTION("shutdown_timeout = ") + { + L.requireFailure("require 'lanes'.configure{shutdown_timeout = -0.001}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("shutdown_timeout = 0") - { - L.requireSuccess("require 'lanes'.configure{shutdown_timeout = 0}"); - } + SECTION("shutdown_timeout = 0") + { + L.requireSuccess("require 'lanes'.configure{shutdown_timeout = 0}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("shutdown_timeout = 1s") - { - L.requireSuccess("require 'lanes'.configure{shutdown_timeout = 1}"); - } + SECTION("shutdown_timeout = 1s") + { + L.requireSuccess("require 'lanes'.configure{shutdown_timeout = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("shutdown_timeout = 3600s") - { - L.requireSuccess("require 'lanes'.configure{shutdown_timeout = 3600}"); - } + SECTION("shutdown_timeout = 3600s") + { + L.requireSuccess("require 'lanes'.configure{shutdown_timeout = 3600}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("shutdown_timeout = ") - { - L.requireFailure("require 'lanes'.configure{shutdown_timeout = 3600.001}"); - } + SECTION("shutdown_timeout = ") + { + L.requireFailure("require 'lanes'.configure{shutdown_timeout = 3600.001}"); } +} + +// ################################################################################################# + +TEST_CASE("lanes.configure.strip_functions") +{ + LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; - // --------------------------------------------------------------------------------------------- // strip_functions should be a boolean - SECTION("strip_functions") + SECTION("strip_functions =
") { - SECTION("strip_functions =
") - { - L.requireFailure("require 'lanes'.configure{strip_functions = {}}"); - } + L.requireFailure("require 'lanes'.configure{strip_functions = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("strip_functions = ") - { - L.requireFailure("require 'lanes'.configure{strip_functions = 'gluh'}"); - } + SECTION("strip_functions = ") + { + L.requireFailure("require 'lanes'.configure{strip_functions = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("strip_functions = ") - { - L.requireFailure("require 'lanes'.configure{strip_functions = 1}"); - } + SECTION("strip_functions = ") + { + L.requireFailure("require 'lanes'.configure{strip_functions = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("strip_functions = ") - { - L.requireFailure("require 'lanes'.configure{strip_functions = print}"); - } + SECTION("strip_functions = ") + { + L.requireFailure("require 'lanes'.configure{strip_functions = print}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("strip_functions = false") - { - L.requireSuccess("require 'lanes'.configure{strip_functions = false}"); - } + SECTION("strip_functions = false") + { + L.requireSuccess("require 'lanes'.configure{strip_functions = false}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("strip_functions = true") - { - L.requireSuccess("require 'lanes'.configure{strip_functions = true}"); - } + SECTION("strip_functions = true") + { + L.requireSuccess("require 'lanes'.configure{strip_functions = true}"); } +} + +// ################################################################################################# + +TEST_CASE("lanes.configure.track_lanes") +{ + LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; - // --------------------------------------------------------------------------------------------- // track_lanes should be a boolean - SECTION("track_lanes") + SECTION("track_lanes =
") { - SECTION("track_lanes =
") - { - L.requireFailure("require 'lanes'.configure{track_lanes = {}}"); - } + L.requireFailure("require 'lanes'.configure{track_lanes = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("track_lanes = ") - { - L.requireFailure("require 'lanes'.configure{track_lanes = 'gluh'}"); - } + SECTION("track_lanes = ") + { + L.requireFailure("require 'lanes'.configure{track_lanes = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("track_lanes = ") - { - L.requireFailure("require 'lanes'.configure{track_lanes = 1}"); - } + SECTION("track_lanes = ") + { + L.requireFailure("require 'lanes'.configure{track_lanes = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("track_lanes = ") - { - L.requireFailure("require 'lanes'.configure{track_lanes = print}"); - } + SECTION("track_lanes = ") + { + L.requireFailure("require 'lanes'.configure{track_lanes = print}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("track_lanes = false") - { - L.requireSuccess("require 'lanes'.configure{track_lanes = false}"); - } + SECTION("track_lanes = false") + { + L.requireSuccess("require 'lanes'.configure{track_lanes = false}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("track_lanes = true") - { - L.requireSuccess("require 'lanes'.configure{track_lanes = true}"); - } + SECTION("track_lanes = true") + { + L.requireSuccess("require 'lanes'.configure{track_lanes = true}"); } +} + +// ################################################################################################# + +TEST_CASE("lanes.configure.verbose_errors") +{ + LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; - // --------------------------------------------------------------------------------------------- // verbose_errors should be a boolean - SECTION("verbose_errors") + SECTION("verbose_errors =
") { - SECTION("verbose_errors =
") - { - L.requireFailure("require 'lanes'.configure{verbose_errors = {}}"); - } + L.requireFailure("require 'lanes'.configure{verbose_errors = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("verbose_errors = ") - { - L.requireFailure("require 'lanes'.configure{verbose_errors = 'gluh'}"); - } + SECTION("verbose_errors = ") + { + L.requireFailure("require 'lanes'.configure{verbose_errors = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("verbose_errors = ") - { - L.requireFailure("require 'lanes'.configure{verbose_errors = 1}"); - } + SECTION("verbose_errors = ") + { + L.requireFailure("require 'lanes'.configure{verbose_errors = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("verbose_errors = ") - { - L.requireFailure("require 'lanes'.configure{verbose_errors = print}"); - } + SECTION("verbose_errors = ") + { + L.requireFailure("require 'lanes'.configure{verbose_errors = print}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("verbose_errors = false") - { - L.requireSuccess("require 'lanes'.configure{verbose_errors = false}"); - } + SECTION("verbose_errors = false") + { + L.requireSuccess("require 'lanes'.configure{verbose_errors = false}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("verbose_errors = true") - { - L.requireSuccess("require 'lanes'.configure{verbose_errors = true}"); - } + SECTION("verbose_errors = true") + { + L.requireSuccess("require 'lanes'.configure{verbose_errors = true}"); } +} + +// ################################################################################################# + +TEST_CASE("lanes.configure.with_timers") +{ + LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; - // --------------------------------------------------------------------------------------------- // with_timers should be a boolean - SECTION("with_timers") + SECTION("with_timers =
") { - SECTION("with_timers =
") - { - L.requireFailure("require 'lanes'.configure{with_timers = {}}"); - } + L.requireFailure("require 'lanes'.configure{with_timers = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("with_timers = ") - { - L.requireFailure("require 'lanes'.configure{with_timers = 'gluh'}"); - } + SECTION("with_timers = ") + { + L.requireFailure("require 'lanes'.configure{with_timers = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("with_timers = ") - { - L.requireFailure("require 'lanes'.configure{with_timers = 1}"); - } + SECTION("with_timers = ") + { + L.requireFailure("require 'lanes'.configure{with_timers = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("with_timers = ") - { - L.requireFailure("require 'lanes'.configure{with_timers = print}"); - } + SECTION("with_timers = ") + { + L.requireFailure("require 'lanes'.configure{with_timers = print}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("with_timers = false") - { - L.requireSuccess("require 'lanes'.configure{with_timers = false}"); - } + SECTION("with_timers = false") + { + L.requireSuccess("require 'lanes'.configure{with_timers = false}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("with_timers = true") - { - L.requireSuccess("require 'lanes'.configure{with_timers = true}"); - } + SECTION("with_timers = true") + { + L.requireSuccess("require 'lanes'.configure{with_timers = true}"); } +} + +// ################################################################################################# + +TEST_CASE("lanes.configure.unknown_setting") +{ + LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; - // --------------------------------------------------------------------------------------------- // any unknown setting should be rejected - SECTION("unknown_setting") + SECTION("table setting") { - SECTION("table setting") - { - L.requireFailure("require 'lanes'.configure{[{}] = {}}"); - } + L.requireFailure("require 'lanes'.configure{[{}] = {}}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("boolean setting") - { - L.requireFailure("require 'lanes'.configure{[true] = 'gluh'}"); - } + SECTION("boolean setting") + { + L.requireFailure("require 'lanes'.configure{[true] = 'gluh'}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("function setting") - { - L.requireFailure("require 'lanes'.configure{[function() end] = 1}"); - } + SECTION("function setting") + { + L.requireFailure("require 'lanes'.configure{[function() end] = 1}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("number setting") - { - L.requireFailure("require 'lanes'.configure{[1] = function() end}"); - } + SECTION("number setting") + { + L.requireFailure("require 'lanes'.configure{[1] = function() end}"); + } - // ----------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------------- - SECTION("unknown string setting") - { - L.requireFailure("require 'lanes'.configure{['gluh'] = false}"); - } + SECTION("unknown string setting") + { + L.requireFailure("require 'lanes'.configure{['gluh'] = false}"); } } diff --git a/unit_tests/linda_tests.cpp b/unit_tests/linda_tests.cpp index f2934eb..9dbaa85 100644 --- a/unit_tests/linda_tests.cpp +++ b/unit_tests/linda_tests.cpp @@ -3,67 +3,132 @@ // ################################################################################################# -TEST_CASE("linda.single Keeper") +TEST_CASE("linda.single_keeper.creation/no_argument") { LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; S.requireSuccess("lanes = require 'lanes'"); - SECTION("Linda creation") - { - // no parameters is ok - S.requireSuccess("lanes.linda()"); - S.requireNotReturnedString("return tostring(lanes.linda())", R"===(Linda: )==="); // unspecified name should not result in - - // since we have only one keeper, only group 0 is authorized - S.requireFailure("lanes.linda(-1)"); - S.requireSuccess("lanes.linda(0)"); - S.requireFailure("lanes.linda(1)"); - - // any name is ok - S.requireSuccess("lanes.linda('')"); // an empty name results in a string conversion of the form "Linda: " that we can't test (but it works) - S.requireReturnedString("return tostring(lanes.linda('short name'))", R"===(Linda: short name)==="); - S.requireReturnedString("return tostring(lanes.linda('very very very very very very long name'))", R"===(Linda: very very very very very very long name)==="); - S.requireReturnedString("return tostring(lanes.linda('auto'))", R"===(Linda: [string "return tostring(lanes.linda('auto'))"]:1)==="); - - if constexpr (LUA_VERSION_NUM == 504) { - // a function is acceptable as a __close handler - S.requireSuccess("local l = lanes.linda(function() end)"); - // a callable table too (a callable full userdata as well, but I have none here) - S.requireSuccess("local l = lanes.linda(setmetatable({}, {__call = function() end}))"); - // if the function raises an error, we should get it - S.requireFailure("local l = lanes.linda(function() error 'gluh' end)"); - } else { - // no __close support before Lua 5.4 - S.requireFailure("lanes.linda(function() end)"); - S.requireFailure("lanes.linda(setmetatable({}, {__call = function() end}))"); - } + // no argument is ok + S.requireSuccess("lanes.linda()"); + S.requireNotReturnedString("return tostring(lanes.linda())", R"===(Linda: )==="); // unspecified name should not result in +} - // mixing parameters in any order is ok: 2 out of 3 - S.requireSuccess("lanes.linda(0, 'name')"); - S.requireSuccess("lanes.linda('name', 0)"); - if constexpr (LUA_VERSION_NUM == 504) { - S.requireSuccess("lanes.linda(0, function() end)"); - S.requireSuccess("lanes.linda(function() end, 0)"); - S.requireSuccess("lanes.linda('name', function() end)"); - S.requireSuccess("lanes.linda(function() end, 'name')"); - } +// ################################################################################################# - // mixing parameters in any order is ok: 3 out of 3 - if constexpr (LUA_VERSION_NUM == 504) { - S.requireSuccess("lanes.linda(0, 'name', function() end)"); - S.requireSuccess("lanes.linda(0, function() end, 'name')"); - S.requireSuccess("lanes.linda('name', 0, function() end)"); - S.requireSuccess("lanes.linda('name', function() end, 0)"); - S.requireSuccess("lanes.linda(function() end, 0, 'name')"); - S.requireSuccess("lanes.linda(function() end, 'name', 0)"); - } +TEST_CASE("linda.single_keeper.creation/non_table_arguments") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); + + // any argument that is not a table is not ok + S.requireFailure("lanes.linda(0)"); + S.requireFailure("lanes.linda('bob')"); + S.requireFailure("lanes.linda(false)"); + S.requireFailure("lanes.linda(function() end)"); + S.requireFailure("lanes.linda(lanes.cancel_error)"); +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.creation/close_handler") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); + + if constexpr (LUA_VERSION_NUM == 504) { + // a function is acceptable as a __close handler + S.requireSuccess("local l = lanes.linda{close_handler = function() end}"); + // a callable table too (a callable full userdata as well, but I have none here) + S.requireSuccess("local l = lanes.linda{close_handler = setmetatable({}, {__call = function() end})}"); + } else { + // no __close support before Lua 5.4, field is ignored (therefore invalid values are accepted too!) + S.requireSuccess("lanes.linda{close_handler = 'a string'}"); + S.requireSuccess("lanes.linda{close_handler = function() end}"); + S.requireSuccess("lanes.linda{close_handler = setmetatable({}, {__call = function() end})}"); + } +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.creation/table_argument") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); + + // one table is fine + S.requireSuccess("lanes.linda{}"); + // anything beyond that is not + S.requireFailure("lanes.linda({},{})"); + S.requireFailure("lanes.linda({},'bob')"); + S.requireFailure("lanes.linda({},42)"); +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.creation/group") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); + + // since we have only one keeper, only group 0 is authorized + S.requireFailure("lanes.linda{group = -1}"); + S.requireSuccess("lanes.linda{group = 0}"); + S.requireFailure("lanes.linda{group = 1}"); +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.creation/name") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); + + // any name is ok + S.requireSuccess("lanes.linda{name = ''}"); // an empty name results in a string conversion of the form "Linda: " that we can't test (but it works) + S.requireReturnedString("return tostring(lanes.linda{name = 'short name'})", R"===(Linda: short name)==="); + S.requireReturnedString("return tostring(lanes.linda{name = 'very very very very very very long name'})", R"===(Linda: very very very very very very long name)==="); + S.requireReturnedString("return tostring(lanes.linda{name = 'auto'})", R"===(Linda: [string "return tostring(lanes.linda{name = 'auto'})"]:1)==="); +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.creation/wake_period") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); - // unsupported parameters should fail - S.requireFailure("lanes.linda(true)"); - S.requireFailure("lanes.linda(false)"); - // uncallable table or full userdata - S.requireFailure("lanes.linda({})"); - S.requireFailure("lanes.linda(lanes.linda())"); + // wake_period should be a number > 0 + S.requireFailure("lanes.linda{wake_period = false}"); + S.requireFailure("lanes.linda{wake_period = 'bob'}"); + S.requireFailure("lanes.linda{wake_period = {}}"); + S.requireFailure("lanes.linda{wake_period = -1}"); + S.requireFailure("lanes.linda{wake_period = 0}"); + S.requireSuccess("lanes.linda{wake_period = 0.0001}"); +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.wake_period") +{ + FAIL("TODO: check that wake_period works as expected"); + // - use configure default if not provided + // - overrides default when provided + // - blocking operation wakes at the specified period +} + +// ################################################################################################# + +TEST_CASE("linda.single_keeper.the_rest") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'"); + + // --------------------------------------------------------------------------------------------- + + SECTION("error in close handler is propagated") + { + // if the function raises an error, we should get it + S.requireFailure("local l = lanes.linda{close_handler = function() error 'gluh' end}"); } // --------------------------------------------------------------------------------------------- @@ -311,12 +376,12 @@ TEST_CASE("linda.multi Keeper") S.requireSuccess("lanes = require 'lanes'.configure{nb_user_keepers = 3}"); - S.requireFailure("lanes.linda(-1)"); - S.requireSuccess("lanes.linda(0)"); - S.requireSuccess("lanes.linda(1)"); - S.requireSuccess("lanes.linda(2)"); - S.requireSuccess("lanes.linda(3)"); - S.requireFailure("lanes.linda(4)"); + S.requireFailure("lanes.linda{group = -1}"); + S.requireSuccess("lanes.linda{group = 0}"); + S.requireSuccess("lanes.linda{group = 1}"); + S.requireSuccess("lanes.linda{group = 2}"); + S.requireSuccess("lanes.linda{group = 3}"); + S.requireFailure("lanes.linda{group = 4}"); } // ################################################################################################# diff --git a/unit_tests/scripts/lane/tasking_cancelling.lua b/unit_tests/scripts/lane/tasking_cancelling.lua index ea4516e..873140e 100644 --- a/unit_tests/scripts/lane/tasking_cancelling.lua +++ b/unit_tests/scripts/lane/tasking_cancelling.lua @@ -16,7 +16,7 @@ local lanes_linda = assert(lanes.linda) -- ################################################################################################## -- cancellation of lanes waiting on a linda -local limited = lanes_linda("limited") +local limited = lanes_linda{name = "limited"} assert.fails(function() limited:limit("key", -1) end) assert.failsnot(function() limited:limit("key", 1) end) -- [[################################################ diff --git a/unit_tests/scripts/lane/tasking_comms_criss_cross.lua b/unit_tests/scripts/lane/tasking_comms_criss_cross.lua index 497e81d..610da8b 100644 --- a/unit_tests/scripts/lane/tasking_comms_criss_cross.lua +++ b/unit_tests/scripts/lane/tasking_comms_criss_cross.lua @@ -42,7 +42,7 @@ local tc = lanes_gen("io", { name = 'auto', gc_cb = gc_cb }, end ) -local linda= lanes_linda("criss cross") +local linda= lanes_linda{name = "criss cross"} local a,b= tc(linda, "A","B"), tc(linda, "B","A") -- launching two lanes, twisted comms diff --git a/unit_tests/scripts/lane/tasking_communications.lua b/unit_tests/scripts/lane/tasking_communications.lua index 631d105..01842b4 100644 --- a/unit_tests/scripts/lane/tasking_communications.lua +++ b/unit_tests/scripts/lane/tasking_communications.lua @@ -72,7 +72,7 @@ local chunk= function(linda) WR("chunk ", "Lane ends!\n") end -local linda = lanes_linda("communications") +local linda = lanes_linda{name = "communications"} assert(type(linda) == "userdata" and tostring(linda) == "Linda: communications") -- -- ["->"] master -> slave diff --git a/unit_tests/scripts/lane/tasking_send_receive_code.lua b/unit_tests/scripts/lane/tasking_send_receive_code.lua index e329a88..cb3663f 100644 --- a/unit_tests/scripts/lane/tasking_send_receive_code.lua +++ b/unit_tests/scripts/lane/tasking_send_receive_code.lua @@ -65,7 +65,7 @@ local function chunk2(linda) linda:send("up", function() return ":)" end, "ok2") end -local linda = lanes_linda("auto") +local linda = lanes_linda{name = "auto"} local t2= lanes_gen("debug,package,string,io", { name = 'auto', gc_cb = gc_cb }, chunk2)(linda) -- prepare & launch linda:send("down", function(linda) linda:send("up", "ready!") end, "ok") diff --git a/unit_tests/scripts/linda/multiple_keepers.lua b/unit_tests/scripts/linda/multiple_keepers.lua index 8733087..267d874 100644 --- a/unit_tests/scripts/linda/multiple_keepers.lua +++ b/unit_tests/scripts/linda/multiple_keepers.lua @@ -2,9 +2,9 @@ local require_lanes_result_1, require_lanes_result_2 = require "lanes".configure{nb_user_keepers = 3, keepers_gc_threshold = 500} local lanes = require_lanes_result_1 -local a = lanes.linda("A", 1) -local b = lanes.linda("B", 2) -local c = lanes.linda("C", 3) +local a = lanes.linda{name = "A", group = 1} +local b = lanes.linda{name = "B", group = 2} +local c = lanes.linda{name = "C", group = 3} -- store each linda in the other 2 do diff --git a/unit_tests/scripts/linda/send_registered_userdata.lua b/unit_tests/scripts/linda/send_registered_userdata.lua index 2c0195a..90c05c9 100644 --- a/unit_tests/scripts/linda/send_registered_userdata.lua +++ b/unit_tests/scripts/linda/send_registered_userdata.lua @@ -1,5 +1,5 @@ local lanes = require 'lanes'.configure{with_timers = false} -local l = lanes.linda'gleh' +local l = lanes.linda{name = 'gleh'} l:set('yo', io.stdin) local n, stdin_out = l:get('yo') assert(n == 1 and stdin_out == io.stdin, tostring(stdin_out) .. " ~= " .. tostring(io.stdin)) -- cgit v1.2.3-55-g6feb