Skip to content

Commit

Permalink
Merge branch 'pcallallthethings' into 'master'
Browse files Browse the repository at this point in the history
Wrap all memory allocating Lua functions in protected calls

Closes #8099

See merge request OpenMW/openmw!4336
  • Loading branch information
psi29a committed Sep 4, 2024
2 parents da0e2e7 + 032d506 commit b3677d0
Show file tree
Hide file tree
Showing 67 changed files with 1,365 additions and 1,191 deletions.
46 changes: 26 additions & 20 deletions apps/components_tests/lua/test_async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ namespace
{
void SetUp() override
{
mLua.sol()["callback"] = [&](sol::protected_function fn) -> LuaUtil::Callback {
sol::table hiddenData(mLua.sol(), sol::create);
hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{};
return LuaUtil::Callback{ std::move(fn), hiddenData };
};
mLua.sol()["pass"] = [this](LuaUtil::Callback callback) { mCb = callback; };
mLua.protectedCall([&](LuaUtil::LuaView& view) {
view.sol()["callback"] = [](sol::this_state state, sol::protected_function fn) -> LuaUtil::Callback {
sol::table hiddenData(state, sol::create);
hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{};
return LuaUtil::Callback{ std::move(fn), hiddenData };
};
view.sol()["pass"] = [&](LuaUtil::Callback callback) { mCb = callback; };
});
}

LuaUtil::LuaState mLua{ nullptr, nullptr };
Expand All @@ -28,25 +30,29 @@ namespace
TEST_F(LuaCoroutineCallbackTest, CoroutineCallbacks)
{
internal::CaptureStdout();
mLua.sol().safe_script(R"X(
local s = 'test'
coroutine.wrap(function()
pass(callback(function(v) print(s) end))
end)()
)X");
mLua.sol().collect_garbage();
mCb.call();
mLua.protectedCall([&](LuaUtil::LuaView& view) {
view.sol().safe_script(R"X(
local s = 'test'
coroutine.wrap(function()
pass(callback(function(v) print(s) end))
end)()
)X");
view.sol().collect_garbage();
mCb.call();
});
EXPECT_THAT(internal::GetCapturedStdout(), "test\n");
}

TEST_F(LuaCoroutineCallbackTest, ErrorInCoroutineCallbacks)
{
mLua.sol().safe_script(R"X(
coroutine.wrap(function()
pass(callback(function() error('COROUTINE CALLBACK') end))
end)()
)X");
mLua.sol().collect_garbage();
mLua.protectedCall([&](LuaUtil::LuaView& view) {
view.sol().safe_script(R"X(
coroutine.wrap(function()
pass(callback(function() error('COROUTINE CALLBACK') end))
end)()
)X");
view.sol().collect_garbage();
});
EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK");
}
}
161 changes: 82 additions & 79 deletions apps/components_tests/lua/test_l10n.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,84 +81,87 @@ you_have_arrows: "Arrows count: {count}"
TEST_F(LuaL10nTest, L10n)
{
LuaUtil::LuaState lua{ mVFS.get(), &mCfg };
sol::state_view& l = lua.sol();
internal::CaptureStdout();
l10n::Manager l10nManager(mVFS.get());
l10nManager.setPreferredLocales({ "de", "en" });
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: gmst de en\n");

l["l10n"] = LuaUtil::initL10nLoader(l, &l10nManager);

internal::CaptureStdout();
l.safe_script("t1 = l10n('Test1')");
EXPECT_THAT(internal::GetCapturedStdout(),
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n");

internal::CaptureStdout();
l.safe_script("t2 = l10n('Test2')");
{
std::string output = internal::GetCapturedStdout();
EXPECT_THAT(output, HasSubstr("Language file \"l10n/Test2/en.yaml\" is enabled"));
}

EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Guten Morgen.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile.");
EXPECT_EQ(get<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");

internal::CaptureStdout();
l10nManager.setPreferredLocales({ "en", "de" });
EXPECT_THAT(internal::GetCapturedStdout(),
"Preferred locales: gmst en de\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test2/en.yaml\" is enabled\n");

EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Good morning.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "You have one arrow.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"male\"})"), "He is coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"female\"})"), "She is coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"blah\"})"), "They are coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"other\"})"), "They are coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=0.1})"), "The quest is 10% complete.");
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=1})"), "The quest is 100% complete.");
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=1})"), "You came in 1st place.");
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=100})"), "You came in 100th place.");
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=1})"), "There is one thing.");
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=100})"), "There are one hundred things.");
EXPECT_EQ(get<std::string>(l, "t1('duration', {num=100})"), "It took 1:40");
EXPECT_EQ(get<std::string>(l, "t1('numbers', {int=123, double=123.456})"),
"123 and 123 are integers, but 123.456 is a double");
EXPECT_EQ(get<std::string>(l, "t1('rounding', {value=123.456789})"), "123.46");
// Check that failed messages display the key instead of an empty string
EXPECT_EQ(get<std::string>(l, "t1('{mismatched_braces')"), "{mismatched_braces");
EXPECT_EQ(get<std::string>(l, "t1('{unknown_arg}')"), "{unknown_arg}");
EXPECT_EQ(get<std::string>(l, "t1('{num, integer}', {num=1})"), "{num, integer}");
// Doesn't give a valid currency symbol with `en`. Not that openmw is designed for real world currency.
l10nManager.setPreferredLocales({ "en-US", "de" });
EXPECT_EQ(get<std::string>(l, "t1('currency', {money=10000.10})"), "You have $10,000.10");
// Note: Not defined in English localisation file, so we fall back to the German before falling back to the key
EXPECT_EQ(get<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");

// Test that locales with variants and country codes fall back to more generic locales
internal::CaptureStdout();
l10nManager.setPreferredLocales({ "en-GB-oed", "de" });
EXPECT_THAT(internal::GetCapturedStdout(),
"Preferred locales: gmst en_GB_OED de\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test2/en.yaml\" is enabled\n");
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");

// Test setting fallback language
l.safe_script("t3 = l10n('Test3', 'de')");
l10nManager.setPreferredLocales({ "en" });
EXPECT_EQ(get<std::string>(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!");
lua.protectedCall([&](LuaUtil::LuaView& view) {
sol::state_view& l = view.sol();
internal::CaptureStdout();
l10n::Manager l10nManager(mVFS.get());
l10nManager.setPreferredLocales({ "de", "en" });
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: gmst de en\n");

l["l10n"] = LuaUtil::initL10nLoader(l, &l10nManager);

internal::CaptureStdout();
l.safe_script("t1 = l10n('Test1')");
EXPECT_THAT(internal::GetCapturedStdout(),
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n");

internal::CaptureStdout();
l.safe_script("t2 = l10n('Test2')");
{
std::string output = internal::GetCapturedStdout();
EXPECT_THAT(output, HasSubstr("Language file \"l10n/Test2/en.yaml\" is enabled"));
}

EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Guten Morgen.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile.");
EXPECT_EQ(get<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");

internal::CaptureStdout();
l10nManager.setPreferredLocales({ "en", "de" });
EXPECT_THAT(internal::GetCapturedStdout(),
"Preferred locales: gmst en de\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test2/en.yaml\" is enabled\n");

EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Good morning.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "You have one arrow.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"male\"})"), "He is coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"female\"})"), "She is coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"blah\"})"), "They are coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"other\"})"), "They are coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=0.1})"), "The quest is 10% complete.");
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=1})"), "The quest is 100% complete.");
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=1})"), "You came in 1st place.");
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=100})"), "You came in 100th place.");
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=1})"), "There is one thing.");
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=100})"), "There are one hundred things.");
EXPECT_EQ(get<std::string>(l, "t1('duration', {num=100})"), "It took 1:40");
EXPECT_EQ(get<std::string>(l, "t1('numbers', {int=123, double=123.456})"),
"123 and 123 are integers, but 123.456 is a double");
EXPECT_EQ(get<std::string>(l, "t1('rounding', {value=123.456789})"), "123.46");
// Check that failed messages display the key instead of an empty string
EXPECT_EQ(get<std::string>(l, "t1('{mismatched_braces')"), "{mismatched_braces");
EXPECT_EQ(get<std::string>(l, "t1('{unknown_arg}')"), "{unknown_arg}");
EXPECT_EQ(get<std::string>(l, "t1('{num, integer}', {num=1})"), "{num, integer}");
// Doesn't give a valid currency symbol with `en`. Not that openmw is designed for real world currency.
l10nManager.setPreferredLocales({ "en-US", "de" });
EXPECT_EQ(get<std::string>(l, "t1('currency', {money=10000.10})"), "You have $10,000.10");
// Note: Not defined in English localisation file, so we fall back to the German before falling back to the
// key
EXPECT_EQ(get<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");

// Test that locales with variants and country codes fall back to more generic locales
internal::CaptureStdout();
l10nManager.setPreferredLocales({ "en-GB-oed", "de" });
EXPECT_THAT(internal::GetCapturedStdout(),
"Preferred locales: gmst en_GB_OED de\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test2/en.yaml\" is enabled\n");
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");

// Test setting fallback language
l.safe_script("t3 = l10n('Test3', 'de')");
l10nManager.setPreferredLocales({ "en" });
EXPECT_EQ(get<std::string>(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!");
});
}
}
45 changes: 23 additions & 22 deletions apps/components_tests/lua/test_lua.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,26 @@ return {

TEST_F(LuaStateTest, ToString)
{
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), 3.14)), "3.14");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), true)), "true");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), 3.14)), "3.14");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), true)), "true");
EXPECT_EQ(LuaUtil::toString(sol::nil), "nil");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), "something")), "\"something\"");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), "something")), "\"something\"");
}

TEST_F(LuaStateTest, Cast)
{
EXPECT_EQ(LuaUtil::cast<int>(sol::make_object(mLua.sol(), 3.14)), 3);
EXPECT_ERROR(
LuaUtil::cast<int>(sol::make_object(mLua.sol(), "3.14")), "Value \"\"3.14\"\" can not be casted to int");
EXPECT_ERROR(LuaUtil::cast<std::string_view>(sol::make_object(mLua.sol(), sol::nil)),
EXPECT_EQ(LuaUtil::cast<int>(sol::make_object(mLua.unsafeState(), 3.14)), 3);
EXPECT_ERROR(LuaUtil::cast<int>(sol::make_object(mLua.unsafeState(), "3.14")),
"Value \"\"3.14\"\" can not be casted to int");
EXPECT_ERROR(LuaUtil::cast<std::string_view>(sol::make_object(mLua.unsafeState(), sol::nil)),
"Value \"nil\" can not be casted to string");
EXPECT_ERROR(LuaUtil::cast<std::string>(sol::make_object(mLua.sol(), sol::nil)),
EXPECT_ERROR(LuaUtil::cast<std::string>(sol::make_object(mLua.unsafeState(), sol::nil)),
"Value \"nil\" can not be casted to string");
EXPECT_ERROR(LuaUtil::cast<sol::table>(sol::make_object(mLua.sol(), sol::nil)),
EXPECT_ERROR(LuaUtil::cast<sol::table>(sol::make_object(mLua.unsafeState(), sol::nil)),
"Value \"nil\" can not be casted to sol::table");
EXPECT_ERROR(LuaUtil::cast<sol::function>(sol::make_object(mLua.sol(), "3.14")),
EXPECT_ERROR(LuaUtil::cast<sol::function>(sol::make_object(mLua.unsafeState(), "3.14")),
"Value \"\"3.14\"\" can not be casted to sol::function");
EXPECT_ERROR(LuaUtil::cast<sol::protected_function>(sol::make_object(mLua.sol(), "3.14")),
EXPECT_ERROR(LuaUtil::cast<sol::protected_function>(sol::make_object(mLua.unsafeState(), "3.14")),
"Value \"\"3.14\"\" can not be casted to sol::function");
}

Expand Down Expand Up @@ -186,21 +186,22 @@ return {
TEST_F(LuaStateTest, ProvideAPI)
{
LuaUtil::LuaState lua(mVFS.get(), &mCfg);
lua.protectedCall([&](LuaUtil::LuaView& view) {
sol::table api1 = LuaUtil::makeReadOnly(view.sol().create_table_with("name", "api1"));
sol::table api2 = LuaUtil::makeReadOnly(view.sol().create_table_with("name", "api2"));

sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1"));
sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2"));

sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api1 } });
sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api1 } });

lua.addCommonPackage("sqrlib", lua.sol().create_table_with("sqr", [](int x) { return x * x; }));
lua.addCommonPackage("sqrlib", view.sol().create_table_with("sqr", [](int x) { return x * x; }));

sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api2 } });
sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api2 } });

EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib");
EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get<int>(), 9);
EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib");
EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get<int>(), 9);

EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");
EXPECT_EQ(LuaUtil::call(script2["apiName"]).get<std::string>(), "api2");
EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");
EXPECT_EQ(LuaUtil::call(script2["apiName"]).get<std::string>(), "api2");
});
}

TEST_F(LuaStateTest, GetLuaVersion)
Expand All @@ -212,7 +213,7 @@ return {
{
auto getMem = [&] {
for (int i = 0; i < 5; ++i)
lua_gc(mLua.sol(), LUA_GCCOLLECT, 0);
lua_gc(mLua.unsafeState(), LUA_GCCOLLECT, 0);
return mLua.getTotalMemoryUsage();
};
int64_t memWithScript;
Expand Down
Loading

0 comments on commit b3677d0

Please sign in to comment.