Files
factorio-scenario-ExpCluster/exp_roles/test/lua/env.lua
T
bbassieandClaude Fable 5 e79822b26e Add a test suite for the roles plugin
Run with `pnpm --filter @expcluster/roles test`, using tap as the runner.

The lua module is exercised for real: each test file runs in its own lua
state, created on first use by the helper, with the factorio surface and the
clusterio modules stubbed. The environment is passed to the chunk as an
argument so no globals are involved. Covers lookups and comparisons, the
permission checks, assignment with confirmation, rejection and local only
roles, jail suppression, the sync entry points, and the holder listings,
including a player holding a role in both the synced and the local list.

The javascript side covers the message records round tripping through their
schemas, the indexed permission encoding, and the seed: every permission it
grants must be defined by exp_scenario, and every parent must exist and have
its permissions carried into its children.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 22:02:51 +00:00

194 lines
6.5 KiB
Lua

--[[-- Test environment for module/control.lua
Stubs the factorio globals and the clusterio modules, then loads a fresh copy
of the roles module. Each call to Env.new() is independent.
Run as a chunk by test/helpers/lua.js, receiving the module directory and
returning this table, which is in turn passed to each test chunk.
]]
local module_root = ... --- @type string
local Env = {}
--- Standard fixture: permission names sent once and referenced by zero based index
Env.permission_names = {
"core.admin", -- 0
"exp_scenario.command.kill", -- 1
"exp_scenario.command.jail", -- 2
"exp_scenario.player.admin", -- 3
"exp_scenario.gui.readme", -- 4
"exp_scenario.command.assign_role", -- 5
}
local function meta(order, extra)
local m = { id = 0, order = order }
for k, v in pairs(extra or {}) do m[k] = v end
return m
end
--- Standard fixture: the roles as the controller would send them on initialise
function Env.role_records()
return {
{ id = 0, name = "Cluster Admin", permissions = { 0 }, meta = meta(1, { short_hand = "SYS" }) },
{ id = 5, name = "Moderator", permissions = { 1, 2, 3, 5 }, meta = meta(2, { color = { r = 0, g = 170, b = 0 } }) },
{ id = 6, name = "Regular", permissions = { 1 }, meta = meta(3) },
{ id = 7, name = "Jail", permissions = {}, meta = meta(4, { priority = 1, block_auto_assign = true }) },
{ id = 1, name = "Player", permissions = { 4 }, meta = meta(5), is_default = true },
{ id = 9, name = "Gone", permissions = {}, meta = meta(6), is_deleted = true },
}
end
function Env.assignment(name, role_ids)
return { name = name, role_ids = role_ids }
end
--- Create an independent environment with a fresh copy of the roles module
function Env.new()
local env = {
events = {}, -- events raised through script.raise_event
printed = {}, -- game.print and player.print messages
sent = {}, -- payloads sent to the controller
sounds = {}, -- sounds played to players
results = {}, -- test results collected by env.check
}
local next_event_id = 100
script = {
generate_event_name = function()
next_event_id = next_event_id + 1
return next_event_id
end,
raise_event = function(id, data)
env.events[#env.events + 1] = { id = id, data = data }
end,
register_metatable = function() end,
}
defines = { events = { on_player_joined_game = 1, on_multiplayer_init = 2 } }
local players_by_name, players_by_index, connected = {}, {}, {}
game = {
tick = 1,
player = nil,
players = players_by_name,
connected_players = connected,
print = function(message) env.printed[#env.printed + 1] = message end,
get_player = function(key) return players_by_name[key] or players_by_index[key] end,
}
local stubs = {
["modules/clusterio/api"] = {
send_json = function(channel, data)
env.sent[#env.sent + 1] = { channel = channel, data = data }
end,
},
["modules/clusterio/compat"] = { script_data = {} },
["modules/exp_util/async"] = {
register = function(callback)
return function(...) return callback(...) end
end,
},
}
require = function(name)
return assert(stubs[name], "Unexpected require: " .. name)
end
--- Add a player to the stubbed game
function env.add_player(name, index, is_connected)
local player = {
name = name,
index = index,
connected = is_connected ~= false,
valid = true,
play_sound = function(opts)
env.sounds[#env.sounds + 1] = name .. ":" .. opts.path
end,
print = function(message)
env.printed[#env.printed + 1] = { to = name, message }
end,
}
players_by_name[name] = player
players_by_index[index] = player
if player.connected then connected[#connected + 1] = player end
return player
end
--- A player object which represents the server
env.server = setmetatable({ index = 0, name = "<server>" }, {
__index = function(_, key) error("Server player field accessed: " .. tostring(key)) end,
})
env.Roles = assert(loadfile(module_root .. "/control.lua"))()
env.Roles.on_server_startup()
--- Get a role by name, failing the test file when it does not exist
function env.R(name)
return (assert(env.Roles.get_role_by_name(name), "No role named " .. name))
end
--- Load the standard fixture and the given assignments
function env.initialise(assignments)
env.Roles.initialise{
permission_names = Env.permission_names,
roles = Env.role_records(),
assignments = assignments or {},
}
end
--- Forget everything recorded so far
function env.reset_log()
env.events, env.printed, env.sent, env.sounds = {}, {}, {}, {}
end
--- Record one test result
function env.check(ok, name, detail)
env.results[#env.results + 1] = { name = name, ok = not not ok, detail = detail }
end
--- Shallow array equality
function env.eq(a, b)
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
if #a ~= #b then return false end
for i = 1, #a do
if a[i] ~= b[i] then return false end
end
return true
end
--- Names of an array of roles
function env.names(roles)
local rtn = {}
for index, role in ipairs(roles) do
rtn[index] = role.name
end
return rtn
end
--- Sorted copy of an array of strings
function env.sorted(list)
local rtn = {}
for index, value in ipairs(list) do rtn[index] = value end
table.sort(rtn)
return rtn
end
return env
end
--- Encode the results of an environment as JSON for the javascript side
function Env.results_json(env)
local function escape(value)
return (value:gsub('[%c"\\]', function(c)
return string.format("\\u%04x", c:byte())
end))
end
local parts = {}
for index, result in ipairs(env.results) do
local detail = result.detail and string.format(',"detail":"%s"', escape(result.detail)) or ""
parts[index] = string.format('{"name":"%s","ok":%s%s}', escape(result.name), tostring(result.ok), detail)
end
return "[" .. table.concat(parts, ",") .. "]"
end
return Env