diff --git a/exp_roles/test/module/assignment.lua b/exp_roles/test/module/assignment.lua index a774af5f..69cfb7b9 100644 --- a/exp_roles/test/module/assignment.lua +++ b/exp_roles/test/module/assignment.lua @@ -1,5 +1,5 @@ --- Tests for the assignment methods of module/control.lua -local Suite = ... --- @type Suite The suite this file adds its tests to, see env.lua +local Suite = ... --- @type Suite The suite this file adds its tests to, see env.lua local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty --- alice is a moderator and bob a regular, with changes sent to the controller @@ -40,7 +40,7 @@ test(".assign() applies locally and is sent to the controller", function(env) }, "the change is announced") eq(env.sounds, { "bob:utility/achievement_unlocked" }, "the assign sound plays") - local sd = env.Roles._script_data() + local sd = env.script_data() eq(sd.local_players.bob, { moderator.id }, "the role is held locally") eq(sd.pending.bob, { moderator.id }, "the role is pending") @@ -54,7 +54,16 @@ test(".assign() defaults the by player to game.player", function(env) local players = setup(env) game.player = players.alice env.R("Jail"):assign(players.bob, { silent = true }) - eq(env.events[1].by_player_index, players.alice.index, "the acting player is game.player") + eq(env.events, { + { + name = env.Roles.events.on_player_roles_changed, + tick = 1, + player_index = players.bob.index, + by_player_index = players.alice.index, + assigned = { env.R("Jail").id }, + unassigned = {}, + }, + }, "the acting player is game.player") end) test("receive_assignment_updates() releases the local hold once confirmed", function(env) @@ -65,7 +74,7 @@ test("receive_assignment_updates() releases the local hold once confirmed", func empty(env.events, "a confirmation raises no events") empty(env.printed, "a confirmation announces nothing") - local sd = env.Roles._script_data() + local sd = env.script_data() check(sd.local_players.bob == nil and sd.pending.bob == nil, "the role is no longer held locally") check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation") end) @@ -113,7 +122,7 @@ test("reject_assignment() rolls the assignment back", function(env) }, }, "the rollback raises the event") - local sd = env.Roles._script_data() + local sd = env.script_data() check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state") end) @@ -124,7 +133,7 @@ test(".assign() with local_only never reaches the controller", function(env) empty(env.sent, "the assignment is not sent") check(regular:has_player(players.alice), "the role applies") - local sd = env.Roles._script_data() + local sd = env.script_data() check(sd.pending.alice == nil, "the role is not pending") eq(sd.local_players.alice, { regular.id }, "the role is held locally") @@ -146,15 +155,31 @@ test(".assign() of a higher priority role suppresses the rest", function(env) check(not env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permissions are lost") check(not env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "the default role is lost") check(not env.Roles.player_outranks(players.alice, players.bob), "a jailed player no longer outranks") - eq(env.events[1].assigned, { jail.id }, "the event lists the jail role") - empty(env.events[1].unassigned, "the suppressed roles are not listed as unassigned") + eq(env.events, { + { + name = env.Roles.events.on_player_roles_changed, + tick = 1, + player_index = players.alice.index, + by_player_index = 0, + assigned = { jail.id }, + unassigned = {}, + }, + }, "the event lists only the jail role") env.reset_log() jail:unassign(players.alice, { by_player_name = "", silent = true }) eq(Suite.sorted(Suite.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" }, "unjail restores the roles") - empty(env.events[1].assigned, "the restored roles are not listed as assigned") - eq(env.events[1].unassigned, { jail.id }, "the unjail event lists the jail role") + eq(env.events, { + { + name = env.Roles.events.on_player_roles_changed, + tick = 1, + player_index = players.alice.index, + by_player_index = 0, + assigned = {}, + unassigned = { jail.id }, + }, + }, "the unjail event lists only the jail role") end) test(".assign() ignores the server", function(env) diff --git a/exp_roles/test/module/env.lua b/exp_roles/test/module/env.lua index 5b612329..d4e3953a 100644 --- a/exp_roles/test/module/env.lua +++ b/exp_roles/test/module/env.lua @@ -47,7 +47,17 @@ for _, record in ipairs(role_records()) do role_ids[record.name] = record.id end +--- The environment given to each test: the stubs extended with a fresh copy +--- of the roles module and the fixture helpers +--- @class ExpRoles.TestEnv : Stubs +--- @field Roles ExpRoles A fresh copy of the roles module +--- @field R fun(name: string): ExpRoles.Role Get a fixture role by name +--- @field assignment fun(player_name: string, role_names: string[]): { name: string, role_ids: number[], is_deleted: boolean? } +--- @field script_data fun(): ExpRoles.ScriptData The module's script data, for asserting on internal state +--- @field initialise fun(assignments: table[]?) Load the standard fixture and the given assignments + return Framework.suite(function(env) + --- @cast env ExpRoles.TestEnv env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))() --- @type ExpRoles env.Roles.on_server_startup() @@ -70,6 +80,12 @@ return Framework.suite(function(env) return { name = player_name, role_ids = ids } end + --- The module's script data, for asserting on internal state + function env.script_data() + --- @diagnostic disable-next-line: access-invisible + return env.Roles._script_data() + end + --- Load the standard fixture and the given assignments function env.initialise(assignments) env.Roles.initialise{ diff --git a/exp_roles/test/module/holders.lua b/exp_roles/test/module/holders.lua index 6d5cdb16..a57c8cac 100644 --- a/exp_roles/test/module/holders.lua +++ b/exp_roles/test/module/holders.lua @@ -1,5 +1,5 @@ --- Tests for listing and printing to the players who hold a role -local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua +local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua local test, check, eq = Suite.test, Suite.check, Suite.eq --- alice and dave are moderators with dave offline, zed has never joined @@ -30,7 +30,7 @@ test(".get_player_names() counts a player in both lists once", function(env) env.R("Regular"):assign(players.alice, { silent = true, local_only = true }) env.Roles.receive_assignment_updates{ env.assignment("alice", { "Moderator", "Regular" }) } - local sd = env.Roles._script_data() + local sd = env.script_data() check(sd.local_players.alice ~= nil and sd.synced_players.alice ~= nil, "the role is held in both lists") eq(Suite.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }, "the player is counted once") end) diff --git a/exp_roles/test/module/lookup.lua b/exp_roles/test/module/lookup.lua index e203fbc6..0efcf1b8 100644 --- a/exp_roles/test/module/lookup.lua +++ b/exp_roles/test/module/lookup.lua @@ -1,5 +1,5 @@ --- Tests for the role lookup functions of module/control.lua -local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua +local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua local test, check, eq = Suite.test, Suite.check, Suite.eq test("get_role() returns the role with the given clusterio id", function(env) diff --git a/exp_roles/test/module/players.lua b/exp_roles/test/module/players.lua index 50513a30..c7e00257 100644 --- a/exp_roles/test/module/players.lua +++ b/exp_roles/test/module/players.lua @@ -1,5 +1,5 @@ --- Tests for the player checks of module/control.lua -local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua +local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua local test, check, eq = Suite.test, Suite.check, Suite.eq --- alice is a moderator, bob is a regular, and carol has only the default role diff --git a/exp_roles/test/module/sync.lua b/exp_roles/test/module/sync.lua index 64daf581..dc04fcbd 100644 --- a/exp_roles/test/module/sync.lua +++ b/exp_roles/test/module/sync.lua @@ -1,16 +1,19 @@ --- Tests for the sync entry points of module/control.lua -local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua +local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty +--- The state given by the admin trigger, reset by setup for every test +local admin_state = {} --- @type table + --- alice is a moderator and carol has only the default role, both connected local function setup(env) local players = { alice = env.add_player("alice"), carol = env.add_player("carol"), } - env.admin_state = {} + admin_state = {} env.Roles.define_permission_trigger("exp_scenario.player.admin", function(player, state) - env.admin_state[player.name] = state + admin_state[player.name] = state end) env.initialise{ env.assignment("alice", { "Moderator" }), @@ -22,10 +25,12 @@ end test("initialise() applies triggers and raises empty events", function(env) setup(env) - eq(env.admin_state, { alice = true, carol = false }, "triggers run for connected players") + eq(admin_state, { alice = true, carol = false }, "triggers run for connected players") eq(#env.events, 2, "the event is raised once per connected player") - empty(env.events[1].assigned, "the events assign nothing") - empty(env.events[1].unassigned, "the events unassign nothing") + for _, event in ipairs(env.events) do + empty(event.assigned, "the events assign nothing") + empty(event.unassigned, "the events unassign nothing") + end empty(env.printed, "initialise is silent") empty(env.sent, "initialise sends nothing") empty(env.sounds, "initialise plays no sounds") @@ -44,7 +49,7 @@ test("initialise() is authoritative for pending roles", function(env) env.R("Moderator"):assign(players.carol) env.R("Jail"):assign(players.carol, { silent = true, local_only = true }) - local sd = env.Roles._script_data() + local sd = env.script_data() eq(sd.pending.carol, { env.R("Moderator").id }, "the role is pending before initialise") env.reset_log() @@ -73,13 +78,22 @@ test("receive_assignment_updates() applies controller changes", function(env) eq(env.printed, { { "exp-roles.game-message-assign", "carol", "Moderator", "" }, }, "the change is announced by the server") - check(env.admin_state.carol == true, "triggers follow the assignment") + check(admin_state.carol == true, "triggers follow the assignment") env.reset_log() env.Roles.receive_assignment_updates{ { name = "carol", role_ids = {}, is_deleted = true } } check(not env.R("Moderator"):has_player(players.carol), "a removal applies") - eq(env.events[1].unassigned, { env.R("Moderator").id }, "the removal raises the event") - check(env.admin_state.carol == false, "triggers follow the removal") + eq(env.events, { + { + name = env.Roles.events.on_player_roles_changed, + tick = 1, + player_index = players.carol.index, + by_player_index = 0, + assigned = {}, + unassigned = { env.R("Moderator").id }, + }, + }, "the removal raises the event") + check(admin_state.carol == false, "triggers follow the removal") end) test("receive_assignment_updates() for players not on this map raises nothing", function(env) @@ -99,7 +113,9 @@ test("receive_role_updates() applies to the holders", function(env) } check(env.R("Regular"):has_permission("exp_scenario.command.jail"), "the permission change applies") eq(#env.events, 2, "a role change raises for every connected player") - empty(env.events[1].assigned, "role change events are empty") + for _, event in ipairs(env.events) do + empty(event.assigned, "role change events are empty") + end env.Roles.receive_role_updates{ { id = regular_id, name = "Regular", permissions = {}, meta = { id = 0, order = 3 }, is_deleted = true }, @@ -146,9 +162,13 @@ end) test("on_player_joined_game() runs the triggers", function(env) local players = setup(env) - env.admin_state.alice = nil - env.Roles.on_player_joined_game{ player_index = players.alice.index } - check(env.admin_state.alice ~= nil, "triggers run when a player joins") + admin_state.alice = nil + env.Roles.on_player_joined_game{ + name = defines.events.on_player_joined_game, + tick = game.tick, + player_index = players.alice.index, + } + check(admin_state.alice ~= nil, "triggers run when a player joins") end) return Suite.run() diff --git a/test/lua/framework.lua b/test/lua/framework.lua index 9fc97207..c9918cd0 100644 --- a/test/lua/framework.lua +++ b/test/lua/framework.lua @@ -26,19 +26,21 @@ local function repr(value, depth) end --- Create a suite of tests, one is made per test file ---- @param extend_env fun(env: table): table Extends the stubs given to each test +--- @generic T : Stubs +--- @param extend_env fun(env: Stubs): T Extends the stubs given to each test +--- @return Suite function Framework.suite(extend_env) - --- @class Suite + --- @class Suite local Suite = { - tests = {}, -- { name, fn } in declaration order - results = {}, -- { name, ok, detail? } accumulated across tests + tests = {}, --- @type { name: string, fn: fun(env: T) }[] In declaration order + results = {}, --- @type { name: string, ok: boolean, detail: string? }[] Accumulated across tests } local current_test --- @type string? --- Declare a named test, its function receives a fresh environment --- @param name string - --- @param fn fun(env: table) + --- @param fn fun(env: T) function Suite.test(name, fn) Suite.tests[#Suite.tests + 1] = { name = name, fn = fn } end diff --git a/test/lua/stubs.lua b/test/lua/stubs.lua index 3cc1b3a8..b18d13ef 100644 --- a/test/lua/stubs.lua +++ b/test/lua/stubs.lua @@ -51,11 +51,15 @@ end --- Create an independent set of stubs, installed as the lua globals function Stubs.new() --- @class Stubs + --- @field events table[] Events raised through script.raise_event, name and tick filled in + --- @field printed (LocalisedString | { to: string, [1]: LocalisedString })[] Messages from game.print and player.print + --- @field sent { channel: string, data: table }[] Payloads sent through the clusterio api + --- @field sounds string[] Sounds played to players, as "player:path" local stubs = { - events = {}, -- events raised through script.raise_event - printed = {}, -- game.print and player.print messages - sent = {}, -- payloads sent through the clusterio api - sounds = {}, -- sounds played to players + events = {}, + printed = {}, + sent = {}, + sounds = {}, } local next_event_id = 100 @@ -97,8 +101,12 @@ function Stubs.new() }, { "player" }) --- Add a player to the stubbed game, indexes are assigned in join order + --- @param name string + --- @param is_connected boolean? Defaults to connected + --- @return Stubs.Player function stubs.add_player(name, is_connected) local index = #players_by_index + 1 + --- @class Stubs.Player local player = Stubs.strict("LuaPlayer " .. name, { name = name, index = index, @@ -118,6 +126,7 @@ function Stubs.new() end --- A player object which represents the server + --- @type Stubs.Player stubs.server = Stubs.strict("LuaPlayer ", { index = 0, name = "" }) --- Modules resolved by the stubbed require, add to them with extend_requires