216 lines
7.5 KiB
C++
216 lines
7.5 KiB
C++
#include <faset/runtime/Runtime.hpp>
|
|
#include <faset/scripting/LuaModule.hpp>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
using namespace faset::runtime;
|
|
using namespace faset::scripting;
|
|
using Json = nlohmann::json;
|
|
|
|
namespace {
|
|
void check(bool value, const char* message) {
|
|
if (!value)
|
|
throw std::runtime_error(message);
|
|
}
|
|
template <class Function> void rejects(Function function, const char* message) {
|
|
bool rejected{};
|
|
try {
|
|
function();
|
|
} catch (const std::exception&) {
|
|
rejected = true;
|
|
}
|
|
check(rejected, message);
|
|
}
|
|
LuaProject project(std::string source) {
|
|
LuaProject result;
|
|
result.scripts = {"Scripts/safety.lua"};
|
|
result.sources.emplace(result.scripts.front(), std::move(source));
|
|
return result;
|
|
}
|
|
Json scene(unsigned count = 1) {
|
|
Json entities = Json::array();
|
|
for (unsigned i = 0; i < count; ++i)
|
|
entities.push_back({{"id", "actor" + std::to_string(i)},
|
|
{"name", "Safety actor"},
|
|
{"parent", nullptr},
|
|
{"components", Json::array({{{"id", "transform"},
|
|
{"type", "faset.transform"},
|
|
{"version", 1},
|
|
{"fields", Json::object()}},
|
|
{{"id", "behavior"},
|
|
{"type", "test.safety"},
|
|
{"version", 1},
|
|
{"fields", {{"fail", i == 0}}}}})}});
|
|
return {{"format", "faset.scene"}, {"version", 1}, {"dimension", 2}, {"entities", entities}};
|
|
}
|
|
void depthAndMetatables() {
|
|
LuaModule lua(project(R"lua(
|
|
assert(setmetatable == nil)
|
|
assert(getmetatable("") == "string")
|
|
local nested = 7
|
|
for i = 1, 24 do nested = {nested} end
|
|
local B = faset.behavior {
|
|
id = "test.safety",
|
|
fields = { nested = { type = "any", default = nested } }
|
|
}
|
|
function B:on_start()
|
|
assert(getmetatable(self) == "faset.BehaviorInstance")
|
|
assert(getmetatable(self.entity) == "faset.Entity")
|
|
local value = self.fields.nested
|
|
for i = 1, 24 do value = value[1] end
|
|
assert(value == 7)
|
|
end
|
|
return B
|
|
)lua"));
|
|
Runtime world;
|
|
lua.registerBehaviors(world);
|
|
world.load(scene());
|
|
check(world.diagnostics().empty(), "deep supported JSON uses reserved Lua stack slots");
|
|
rejects(
|
|
[] {
|
|
LuaModule invalid(project(R"lua(
|
|
local nested = 7
|
|
for i = 1, 40 do nested = {nested} end
|
|
return faset.behavior {id="test.safety",fields={value={type="any",default=nested}}}
|
|
)lua"));
|
|
},
|
|
"over-depth JSON rejects cleanly");
|
|
rejects(
|
|
[] {
|
|
LuaModule invalid(project(R"lua(
|
|
local cyclic = {}
|
|
cyclic.self = cyclic
|
|
return faset.behavior {id="test.safety",fields={value={type="any",default=cyclic}}}
|
|
)lua"));
|
|
},
|
|
"cyclic JSON rejects cleanly");
|
|
}
|
|
void jsonFanout() {
|
|
// Lua owns only one 1-MiB string. Copying aliases into native JSON must not
|
|
// evade the host's aggregate byte limit and expand into an enormous tree.
|
|
rejects(
|
|
[] {
|
|
LuaModule invalid(project(R"lua(
|
|
local text = string.rep("x", 1024 * 1024)
|
|
local aliases = {}
|
|
for i = 1, 64 do aliases[i] = text end
|
|
return faset.behavior {id="test.safety",fields={value={type="array",default=aliases}}}
|
|
)lua"));
|
|
},
|
|
"JSON string alias fanout is bounded before native copies");
|
|
rejects(
|
|
[] {
|
|
LuaModule invalid(project(R"lua(
|
|
local key = string.rep("x", 1024 * 1024)
|
|
local aliases = {}
|
|
for i = 1, 64 do aliases[i] = {[key]=true} end
|
|
return faset.behavior {id="test.safety",fields={value={type="array",default=aliases}}}
|
|
)lua"));
|
|
},
|
|
"JSON key alias fanout is bounded before native copies");
|
|
}
|
|
void structuralBudgets() {
|
|
for (const char* body : {"for i=1,2048 do self.entity:destroy() end",
|
|
"local text=string.rep('x',1024*1024)\n"
|
|
"for i=1,64 do self.entity:add_component {id='data'..i,type='data'..i,"
|
|
"version=1,fields={text=text}} end"}) {
|
|
LuaModule lua(project(std::string("local B=faset.behavior{id='test.safety',fields={}}\n"
|
|
"function B:on_start()\n") +
|
|
body + "\nend\nreturn B"));
|
|
Runtime world;
|
|
lua.registerBehaviors(world);
|
|
world.load(scene());
|
|
check(world.diagnostics().size() == 1,
|
|
"native structural queue has count and byte budgets");
|
|
world.advance(0.001);
|
|
check(world.diagnostics().size() == 1,
|
|
"structural-budget error disables only that instance");
|
|
}
|
|
}
|
|
void repeatedMemoryFailures() {
|
|
LuaLimits limits;
|
|
limits.memoryBytes = 256 * 1024;
|
|
for (unsigned repetition = 0; repetition < 32; ++repetition) {
|
|
LuaModule lua(project(R"lua(
|
|
local B = faset.behavior {
|
|
id = "test.safety", fields = { fail = { type = "boolean", default = false } }
|
|
}
|
|
function B:update(dt)
|
|
if self.fields.fail then
|
|
self.state.too_large = string.rep("x", 1024 * 1024)
|
|
else
|
|
local value = self.entity:transform()
|
|
value.position.x = value.position.x + 1
|
|
self.entity:set_transform(value)
|
|
end
|
|
end
|
|
return B
|
|
)lua"),
|
|
limits);
|
|
Runtime world;
|
|
lua.registerBehaviors(world);
|
|
world.load(scene(2));
|
|
for (unsigned frame = 0; frame < 20; ++frame)
|
|
world.advance(0.001);
|
|
check(world.diagnostics().size() == 1, "OOM instance reports once across repeated frames");
|
|
check(world.transform(world.find("actor1")).position[0] == 20,
|
|
"OOM does not poison subsequent protected calls or Lua stack");
|
|
}
|
|
// VM bootstrap itself must report allocation failure, not invoke Lua panic.
|
|
limits.memoryBytes = 1;
|
|
rejects(
|
|
[&] {
|
|
LuaModule invalid(project("return faset.behavior{id='test.safety',fields={}}"), limits);
|
|
},
|
|
"tiny VM budget fails safely during construction");
|
|
}
|
|
void persistentStateMemoryFailure() {
|
|
LuaLimits limits;
|
|
limits.memoryBytes = 256 * 1024;
|
|
LuaModule lua(project(R"lua(
|
|
local B = faset.behavior {
|
|
id = "test.safety", fields = { fail = { type = "boolean", default = false } }
|
|
}
|
|
function B:update(dt)
|
|
if self.fields.fail then
|
|
self.state.allocations = {}
|
|
while true do
|
|
self.state.allocations[#self.state.allocations + 1] = string.rep("x", 1024)
|
|
end
|
|
else
|
|
local value = self.entity:transform()
|
|
value.position.x = value.position.x + 1
|
|
self.entity:set_transform(value)
|
|
end
|
|
end
|
|
return B
|
|
)lua"),
|
|
limits);
|
|
Runtime world;
|
|
lua.registerBehaviors(world);
|
|
world.load(scene(2));
|
|
for (unsigned frame = 0; frame < 20; ++frame)
|
|
world.advance(0.001);
|
|
check(world.diagnostics().size() == 1,
|
|
"incremental state exhaustion only disables the failed instance");
|
|
check(world.transform(world.find("actor1")).position[0] == 20,
|
|
"failed state is released before healthy instances run");
|
|
}
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
depthAndMetatables();
|
|
jsonFanout();
|
|
structuralBudgets();
|
|
repeatedMemoryFailures();
|
|
persistentStateMemoryFailure();
|
|
std::cout << "Lua safety tests passed\n";
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::cerr << "Lua safety failure: " << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|