From c52571736d852d2636bd285d19c613be5c706cff Mon Sep 17 00:00:00 2001 From: Benoit Germain Date: Sat, 20 Sep 2025 14:30:57 +0200 Subject: Improve table and userdata conversions * add convert_fallback and convert_max_attempts to global settings * if no __lanesconvert is available, use convert_fallback (can be useful for externally provided full userdata with fixed metatables) * only try conversion on non-deep and non-clonable userdata * conversion can be applied recursively, up to convert_max_attempts times * plus all the relevant unit tests of course --- unit_tests/misc_tests.cpp | 193 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 unit_tests/misc_tests.cpp (limited to 'unit_tests/misc_tests.cpp') diff --git a/unit_tests/misc_tests.cpp b/unit_tests/misc_tests.cpp new file mode 100644 index 0000000..a2199aa --- /dev/null +++ b/unit_tests/misc_tests.cpp @@ -0,0 +1,193 @@ +#include "_pch.hpp" + +#include "shared.h" + +// ################################################################################################# +// ################################################################################################# + +TEST_CASE("misc.__lanesconvert.for_tables") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'.configure()"); + + S.requireSuccess( + " l = lanes.linda()" + " t = setmetatable({}, {__lanesconvert = lanes.null})" // table with a nil-converter + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'nil', 'got ' .. key .. ' ' .. tostring(out))" // should have become nil + ); + + S.requireSuccess( + " l = lanes.linda()" + " t = setmetatable({}, {__lanesconvert = 'decay'})" // table with a decay-converter + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'userdata', 'got ' .. key .. ' ' .. tostring(out))" // should have become a light userdata + ); + + S.requireSuccess( + " l = lanes.linda()" + " t = setmetatable({}, {__lanesconvert = function(t, hint) return 'keeper' end})" // table with a string-converter + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and out == 'keeper')" // should become 'keeper', the hint that the function received + ); + + // make sure that a function that returns the original object causes an error (we don't want infinite loops during conversion) + S.requireFailure( + " l = lanes.linda()" + " t = setmetatable({}, {__lanesconvert = function(t, hint) return t end})" // table with a string-converter + " l:send('k', t)" // send the table, it should raise an error because the converter triggers an infinite loop + ); +} + +// ################################################################################################# +// ################################################################################################# + +TEST_CASE("misc.__lanesconvert.for_userdata") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'.configure()"); + S.requireSuccess("fixture = require 'fixture'"); + + S.requireSuccess("u_tonil = fixture.newuserdata{__lanesconvert = lanes.null}; assert(type(u_tonil) == 'userdata')"); + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', u_tonil)" // send a full userdata with a nil-converter + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'nil')" // should become nil + ); + + S.requireSuccess("u_tolud = fixture.newuserdata{__lanesconvert = 'decay'}; assert(type(u_tolud) == 'userdata')"); + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', u_tolud)" // send a full userdata with a decay-converter + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'userdata' and getmetatable(out) == nil)" // should become a light userdata + ); + + S.requireSuccess("u_tostr = fixture.newuserdata{__lanesconvert = function() return 'yo' end}; assert(type(u_tostr) == 'userdata')"); + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', u_tostr)" // send a full userdata with a string-converter + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and out == 'yo')" // should become 'yo' + ); + + // make sure that a function that returns the original object causes an error (we don't want infinite loops during conversion) + S.requireSuccess("u_toself = fixture.newuserdata{__lanesconvert = function(u) return u end}; assert(type(u_toself) == 'userdata')"); + S.requireFailure( + " l = lanes.linda()" + " l:send('k', u_toself)" // send the userdata, it should raise an error because the converter triggers an infinite loop + ); + + // TODO: make sure that a deep userdata with a __lanesconvert isn't converted (because deep copy takes precedence) +} + +// ################################################################################################# +// ################################################################################################# + +TEST_CASE("misc.convert_fallback.unset") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'.configure()"); + + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', {})" // send a table without a metatable + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'table')" // should not change + ); + + S.requireSuccess("fixture = require 'fixture'; u = fixture.newuserdata(); assert(type(u) == 'userdata')"); + S.requireFailure( + " l = lanes.linda()" + " l:send('k', u)" // send a full userdata without a metatable, should fail + ); +} + +// ################################################################################################# +// ################################################################################################# + +TEST_CASE("misc.convert_fallback.decay") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } }; + S.requireSuccess("lanes = require 'lanes'.configure{convert_fallback = 'decay'}"); + S.requireSuccess("fixture = require 'fixture'"); + + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', {})" // send a table without a metatable + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'userdata' and getmetatable(out) == nil)" // should have become a light userdata + ); + + S.requireSuccess("u = fixture.newuserdata(); assert(type(u) == 'userdata')"); + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', u)" // send a non-copyable non-deep full userdata + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'userdata' and getmetatable(out) == nil)" // should have become a light userdata + ); +} + +// ################################################################################################# +// ################################################################################################# + +TEST_CASE("misc.convert_fallback.convert_no_nil") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; + S.requireSuccess("lanes = require 'lanes'; lanes.configure{convert_fallback = lanes.null}"); + + S.requireSuccess( + " l = lanes.linda()" + " l:send('k', {})" // send a table without a metatable + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'nil')" // should have become nil + ); + + S.requireSuccess( + " l = lanes.linda()" + " t = setmetatable({}, {__lanesconvert = 'decay'})" // override global converter with our own + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and type(out) == 'userdata', 'got ' .. key .. ' ' .. tostring(out))" // should have become a light userdata + ); +} + +// ################################################################################################# +// ################################################################################################# + +TEST_CASE("misc.convert_max_attempts.is_respected") +{ + LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; + S.requireSuccess("lanes = require 'lanes'; lanes.configure{convert_max_attempts = 3}"); + S.requireSuccess("l = lanes.linda()"); + + S.requireSuccess( + " t = setmetatable({n=1}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and out == 'done', 'got ' .. key .. ' ' .. tostring(out))" // should have stayed a table + ); + + S.requireSuccess( + " t = setmetatable({n=2}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and out == 'done', 'got ' .. key .. ' ' .. tostring(out))" // should have stayed a table + ); + + S.requireSuccess( + " t = setmetatable({n=3}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter + " l:send('k', t)" // send the table + " key, out = l:receive('k')" // read it back + " assert(key == 'k' and out == 'done', 'got ' .. key .. ' ' .. tostring(out))" // should have stayed a table + ); + + S.requireFailure( + " t = setmetatable({n=4}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter + " l:send('k', t)" // send the table, it should raise an error because the converter retries too many times + ); +} -- cgit v1.2.3-55-g6feb