mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-09-21 17:04:00 +00:00
Address review on the test framework
- The registry is a Suite, created around the environment factory it runs its tests with, so the plugin's env.lua reads as: build environments, hand the test files a suite of them. The suite returned there becomes `...` in each test file, which is now said where it happens. - pass and fail are the primitives every other check goes through. eq and deep_eq assert rather than compare, failing with both values in the detail. - Stubs are extended through extend_requires, extend_script, extend_game and extend_defines, a recursive merge which raises when a value already exists, rather than by mutating the tables. The strict labels carry the factorio class names. - The stubs record registered metatables and can save and load the script data the way factorio does: functions are refused and only registered metatables survive. env.save_load() uses it, and a new on_load test shows role methods and held roles surviving the round trip, which only passes because the module registers its metatable. - The names helper moved out of the shared framework, it is role specific. - Tests are named after the function or method they cover, on both sides: the lua tests read as "role:assign applies locally and is sent", and the javascript files wrap their tests in the class they exercise with subtests per method. module.test.js is control.test.js, after the file it covers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
24d36db601
commit
7adcdde9dc
@@ -6,6 +6,9 @@ const { reportLuaTests } = require("../../test/lua/runner");
|
|||||||
const envFile = path.join(__dirname, "lua", "env.lua");
|
const envFile = path.join(__dirname, "lua", "env.lua");
|
||||||
|
|
||||||
// Each file runs in its own lua state, and each test in a fresh environment
|
// Each file runs in its own lua state, and each test in a fresh environment
|
||||||
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
|
t.test("module/control.lua", t2 => {
|
||||||
t.test(file, subtest => reportLuaTests(subtest, envFile, path.join(__dirname, "lua", file)));
|
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
|
||||||
}
|
t2.test(`lua/${file}`, subtest => reportLuaTests(subtest, envFile, path.join(__dirname, "lua", file)));
|
||||||
|
}
|
||||||
|
t2.end();
|
||||||
|
});
|
||||||
@@ -65,7 +65,8 @@ async function startPlugin(t2, { roles = [], users = [], config = {} } = {}) {
|
|||||||
|
|
||||||
const role = (id, name, permissions = []) => new lib.Role(id, name, "", new Set(permissions));
|
const role = (id, name, permissions = []) => new lib.Role(id, name, "", new Set(permissions));
|
||||||
|
|
||||||
t.test("init creates properties for existing roles and sweeps orphans", async t2 => {
|
t.test("class ControllerPlugin", t2 => {
|
||||||
|
t2.test("init and sweepRoleMeta manage the role properties", async t2 => {
|
||||||
const { plugin } = await startPlugin(t2, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
|
const { plugin } = await startPlugin(t2, { roles: [role(0, "Cluster Admin"), role(5, "Moderator")] });
|
||||||
|
|
||||||
t2.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first");
|
t2.strictSame(plugin.roleMeta.get(0).order, 1, "the first role is ordered first");
|
||||||
@@ -75,9 +76,9 @@ t.test("init creates properties for existing roles and sweeps orphans", async t2
|
|||||||
plugin.sweepRoleMeta();
|
plugin.sweepRoleMeta();
|
||||||
t2.notOk(plugin.roleMeta.get(99), "properties without a role are removed");
|
t2.notOk(plugin.roleMeta.get(99), "properties without a role are removed");
|
||||||
t2.ok(plugin.roleMeta.get(5), "properties with a role are kept");
|
t2.ok(plugin.roleMeta.get(5), "properties with a role are kept");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("role records combine the role with its properties", async t2 => {
|
t2.test("buildRoleRecord combines the role with its properties", async t2 => {
|
||||||
const { plugin } = await startPlugin(t2, {
|
const { plugin } = await startPlugin(t2, {
|
||||||
roles: [role(1, "Player"), role(5, "Moderator", ["exp_scenario.command.kill"])],
|
roles: [role(1, "Player"), role(5, "Moderator", ["exp_scenario.command.kill"])],
|
||||||
});
|
});
|
||||||
@@ -89,9 +90,9 @@ t.test("role records combine the role with its properties", async t2 => {
|
|||||||
t2.strictSame(record.meta.shortHand, "Mod", "the properties come from the datastore");
|
t2.strictSame(record.meta.shortHand, "Mod", "the properties come from the datastore");
|
||||||
t2.notOk(record.isDefault, "not the default role");
|
t2.notOk(record.isDefault, "not the default role");
|
||||||
t2.ok(plugin.buildRoleRecord(plugin.controller.roles.get(1)).isDefault, "the default role is marked");
|
t2.ok(plugin.buildRoleRecord(plugin.controller.roles.get(1)).isDefault, "the default role is marked");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("role changes broadcast to subscribers", async t2 => {
|
t2.test("rolesUpdated and roleMetaUpdated broadcast to subscribers", async t2 => {
|
||||||
const { plugin, controller, state } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
const { plugin, controller, state } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
||||||
|
|
||||||
state.broadcasts.length = 0;
|
state.broadcasts.length = 0;
|
||||||
@@ -108,9 +109,9 @@ t.test("role changes broadcast to subscribers", async t2 => {
|
|||||||
state.broadcasts.length = 0;
|
state.broadcasts.length = 0;
|
||||||
await plugin.onControllerConfigFieldChanged("controller.default_role_id");
|
await plugin.onControllerConfigFieldChanged("controller.default_role_id");
|
||||||
t2.ok(state.broadcasts.some(event => event instanceof messages.RoleUpdatedEvent), "a default role change rebroadcasts");
|
t2.ok(state.broadcasts.some(event => event instanceof messages.RoleUpdatedEvent), "a default role change rebroadcasts");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("subscriptions replay only newer records", async t2 => {
|
t2.test("handleRoleSubscription replays only newer records", async t2 => {
|
||||||
const { plugin, controller } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
const { plugin, controller } = await startPlugin(t2, { roles: [role(1, "Player")] });
|
||||||
controller.roles.set(role(5, "Moderator"));
|
controller.roles.set(role(5, "Moderator"));
|
||||||
const updatedAtMs = plugin.buildRoleRecord(controller.roles.get(5)).updatedAtMs;
|
const updatedAtMs = plugin.buildRoleRecord(controller.roles.get(5)).updatedAtMs;
|
||||||
@@ -119,9 +120,9 @@ t.test("subscriptions replay only newer records", async t2 => {
|
|||||||
t2.strictSame(all.updates.length, 2, "everything is replayed from the start");
|
t2.strictSame(all.updates.length, 2, "everything is replayed from the start");
|
||||||
const none = await plugin.handleRoleSubscription({ lastRequestTimeMs: updatedAtMs });
|
const none = await plugin.handleRoleSubscription({ lastRequestTimeMs: updatedAtMs });
|
||||||
t2.strictSame(none, null, "nothing is replayed when up to date");
|
t2.strictSame(none, null, "nothing is replayed when up to date");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("assignments mirror users without the default role", async t2 => {
|
t2.test("listAssignmentRecords mirrors users without the default role", async t2 => {
|
||||||
const alice = new FakeUser("alice", [lib.Role.DefaultPlayerRoleId, 5]);
|
const alice = new FakeUser("alice", [lib.Role.DefaultPlayerRoleId, 5]);
|
||||||
alice.updatedAtMs = 10;
|
alice.updatedAtMs = 10;
|
||||||
const bob = new FakeUser("bob", [lib.Role.DefaultPlayerRoleId]);
|
const bob = new FakeUser("bob", [lib.Role.DefaultPlayerRoleId]);
|
||||||
@@ -131,9 +132,9 @@ t.test("assignments mirror users without the default role", async t2 => {
|
|||||||
t2.strictSame(records.length, 1, "users with only the default role are left out");
|
t2.strictSame(records.length, 1, "users with only the default role are left out");
|
||||||
t2.strictSame(records[0].name, "alice");
|
t2.strictSame(records[0].name, "alice");
|
||||||
t2.strictSame([...records[0].roleIds], [5], "the default role is not listed");
|
t2.strictSame([...records[0].roleIds], [5], "the default role is not listed");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("assignment updates validate the user and roles", async t2 => {
|
t2.test("handleAssignmentUpdateRequest validates the user and roles", async t2 => {
|
||||||
const alice = new FakeUser("alice", [5]);
|
const alice = new FakeUser("alice", [5]);
|
||||||
const { plugin, state } = await startPlugin(t2, {
|
const { plugin, state } = await startPlugin(t2, {
|
||||||
roles: [role(1, "Player"), role(5, "Moderator"), role(6, "Regular")], users: [alice],
|
roles: [role(1, "Player"), role(5, "Moderator"), role(6, "Regular")], users: [alice],
|
||||||
@@ -151,9 +152,9 @@ t.test("assignment updates validate the user and roles", async t2 => {
|
|||||||
await plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("alice", [6], [5]));
|
await plugin.handleAssignmentUpdateRequest(new messages.AssignmentUpdateRequest("alice", [6], [5]));
|
||||||
t2.strictSame([...alice.roleIds], [6], "roles are added and removed");
|
t2.strictSame([...alice.roleIds], [6], "roles are added and removed");
|
||||||
t2.ok(state.permissionUpdates.includes("alice"), "permission changes are pushed to the user");
|
t2.ok(state.permissionUpdates.includes("alice"), "permission changes are pushed to the user");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("roles are granted from online time", async t2 => {
|
t2.test("applyAutoAssign and onPlayerEvent grant roles from online time", async t2 => {
|
||||||
const newcomer = new FakeUser("newcomer");
|
const newcomer = new FakeUser("newcomer");
|
||||||
newcomer.playerStats.onlineTimeMs = 3600000;
|
newcomer.playerStats.onlineTimeMs = 3600000;
|
||||||
const veteran = new FakeUser("veteran");
|
const veteran = new FakeUser("veteran");
|
||||||
@@ -175,9 +176,9 @@ t.test("roles are granted from online time", async t2 => {
|
|||||||
veteran.roleIds.delete(6);
|
veteran.roleIds.delete(6);
|
||||||
await plugin.onPlayerEvent(undefined, { type: "leave", name: "veteran" });
|
await plugin.onPlayerEvent(undefined, { type: "leave", name: "veteran" });
|
||||||
t2.ok(veteran.roleIds.has(6), "granted again when the player leaves");
|
t2.ok(veteran.roleIds.has(6), "granted again when the player leaves");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("seeding creates the roles and reuses them by name", async t2 => {
|
t2.test("handleSeedRolesRequest creates the roles and reuses them by name", async t2 => {
|
||||||
const { plugin, controller } = await startPlugin(t2, {
|
const { plugin, controller } = await startPlugin(t2, {
|
||||||
roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")],
|
roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")],
|
||||||
});
|
});
|
||||||
@@ -192,4 +193,6 @@ t.test("seeding creates the roles and reuses them by name", async t2 => {
|
|||||||
|
|
||||||
await plugin.handleSeedRolesRequest();
|
await plugin.handleSeedRolesRequest();
|
||||||
t2.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
t2.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles");
|
||||||
|
});
|
||||||
|
t2.end();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ function decodeRcon(command) {
|
|||||||
return { receiver: match[1], payload: JSON.parse(match[2]) };
|
return { receiver: match[1], payload: JSON.parse(match[2]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
t.test("start subscribes and initialises the lua module", async t2 => {
|
t.test("class InstancePlugin", t2 => {
|
||||||
|
t2.test("onStart subscribes and initialises the lua module", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||||
await plugin.onStart();
|
await plugin.onStart();
|
||||||
|
|
||||||
@@ -73,30 +74,30 @@ t.test("start subscribes and initialises the lua module", async t2 => {
|
|||||||
const emit = decodeRcon(state.rcons[1]);
|
const emit = decodeRcon(state.rcons[1]);
|
||||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
|
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "enabled does not emit changes back");
|
||||||
t2.strictSame(state.warnings.length, 0, "no warnings with a default role");
|
t2.strictSame(state.warnings.length, 0, "no warnings with a default role");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("bidirectional emits changes back", async t2 => {
|
t2.test("onStart with bidirectional emits changes back", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2);
|
const { plugin, state } = await startPlugin(t2);
|
||||||
await plugin.onStart();
|
await plugin.onStart();
|
||||||
const emit = decodeRcon(state.rcons[state.rcons.length - 1]);
|
const emit = decodeRcon(state.rcons[state.rcons.length - 1]);
|
||||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
|
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("disabled does nothing on start", async t2 => {
|
t2.test("onStart with disabled does nothing", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
||||||
await plugin.onStart();
|
await plugin.onStart();
|
||||||
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
||||||
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("a missing default role is warned about", async t2 => {
|
t2.test("onStart warns when no default role is set", async t2 => {
|
||||||
const roles = sampleRoles().map(record => { record.isDefault = false; return record; });
|
const roles = sampleRoles().map(record => { record.isDefault = false; return record; });
|
||||||
const { plugin, state } = await startPlugin(t2, { roles });
|
const { plugin, state } = await startPlugin(t2, { roles });
|
||||||
await plugin.onStart();
|
await plugin.onStart();
|
||||||
t2.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
|
t2.ok(state.warnings.some(message => /default role/.test(message)), "the warning names the default role");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("updates from the controller are forwarded to lua", async t2 => {
|
t2.test("handleRoleUpdatedEvent and handleAssignmentUpdatedEvent forward to lua", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2);
|
const { plugin, state } = await startPlugin(t2);
|
||||||
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
||||||
const roles = decodeRcon(state.rcons[0]);
|
const roles = decodeRcon(state.rcons[0]);
|
||||||
@@ -106,16 +107,16 @@ t.test("updates from the controller are forwarded to lua", async t2 => {
|
|||||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||||
const assignments = decodeRcon(state.rcons[1]);
|
const assignments = decodeRcon(state.rcons[1]);
|
||||||
t2.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
t2.strictSame(assignments.receiver, "receive_assignment_updates", "assignment updates are forwarded");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("updates are not forwarded while disabled", async t2 => {
|
t2.test("handleRoleUpdatedEvent and handleAssignmentUpdatedEvent are gated while disabled", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
const { plugin, state } = await startPlugin(t2, { syncMode: "disabled" });
|
||||||
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
await plugin.handleRoleUpdatedEvent(new messages.RoleUpdatedEvent(sampleRoles()));
|
||||||
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
await plugin.handleAssignmentUpdatedEvent(new messages.AssignmentUpdatedEvent(sampleAssignments()));
|
||||||
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
t2.strictSame(state.rcons.length, 0, "no commands are run");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("in game changes are sent up and rolled back when refused", async t2 => {
|
t2.test("handleAssignmentUpdateIPC sends the change and rolls back a refusal", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2);
|
const { plugin, state } = await startPlugin(t2);
|
||||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||||
const request = state.sent[0];
|
const request = state.sent[0];
|
||||||
@@ -127,17 +128,19 @@ t.test("in game changes are sent up and rolled back when refused", async t2 => {
|
|||||||
const rollback = decodeRcon(state.rcons[0]);
|
const rollback = decodeRcon(state.rcons[0]);
|
||||||
t2.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
|
t2.strictSame(rollback.receiver, "reject_assignment", "the refusal is rolled back in lua");
|
||||||
t2.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
|
t2.strictSame(rollback.payload, { name: "alice", role_ids: [5] }, "the rollback names the refused roles");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("in game changes are dropped unless bidirectional", async t2 => {
|
t2.test("handleAssignmentUpdateIPC is gated unless bidirectional", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
const { plugin, state } = await startPlugin(t2, { syncMode: "enabled" });
|
||||||
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
await plugin.handleAssignmentUpdateIPC({ name: "alice", assign: [5], unassign: undefined });
|
||||||
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
t2.strictSame(state.sent.length, 0, "nothing is sent");
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("changing the sync mode toggles emit", async t2 => {
|
t2.test("onInstanceConfigFieldChanged toggles emit with the sync mode", async t2 => {
|
||||||
const { plugin, state } = await startPlugin(t2);
|
const { plugin, state } = await startPlugin(t2);
|
||||||
await plugin.onInstanceConfigFieldChanged("exp_roles.sync_mode", "enabled", "bidirectional");
|
await plugin.onInstanceConfigFieldChanged("exp_roles.sync_mode", "enabled", "bidirectional");
|
||||||
const emit = decodeRcon(state.rcons[0]);
|
const emit = decodeRcon(state.rcons[0]);
|
||||||
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
|
t2.strictSame([emit.receiver, emit.payload], ["set_emit_events", false], "leaving bidirectional stops emitting");
|
||||||
|
});
|
||||||
|
t2.end();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
--- Assignment through role methods, confirmation, and rejection
|
--- Tests for the assignment methods of module/control.lua
|
||||||
local Env = ...
|
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||||
local Test = Env.Test
|
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||||
local test, check, eq = Test.test, Test.check, Test.eq
|
|
||||||
|
|
||||||
--- alice is a moderator and bob a regular, with changes sent to the controller
|
--- alice is a moderator and bob a regular, with changes sent to the controller
|
||||||
local function setup(env)
|
local function setup(env)
|
||||||
@@ -10,38 +9,41 @@ local function setup(env)
|
|||||||
bob = env.add_player("bob", 2),
|
bob = env.add_player("bob", 2),
|
||||||
}
|
}
|
||||||
env.initialise{
|
env.initialise{
|
||||||
Env.assignment("alice", { 5 }),
|
env.assignment("alice", { 5 }),
|
||||||
Env.assignment("bob", { 6 }),
|
env.assignment("bob", { 6 }),
|
||||||
}
|
}
|
||||||
env.Roles.set_emit_events(true)
|
env.Roles.set_emit_events(true)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
return players
|
return players
|
||||||
end
|
end
|
||||||
|
|
||||||
test("assign applies locally and is sent to the controller", function(env)
|
test("role:assign applies locally and is sent to the controller", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Moderator"):assign(players.bob, { by_player_name = "alice" })
|
env.R("Moderator"):assign(players.bob, { by_player_name = "alice" })
|
||||||
check(#env.sent == 1 and env.sent[1].channel == "exp_roles:assignment_update", "the assignment is sent")
|
check(#env.sent == 1 and env.sent[1].channel == "exp_roles:assignment_update", "the assignment is sent")
|
||||||
check(eq(env.sent[1].data.assign, { 5 }) and env.sent[1].data.name == "bob", "the payload names the role and player")
|
check(env.sent[1].data.name == "bob", "the payload names the player")
|
||||||
|
eq(env.sent[1].data.assign, { 5 }, "the payload names the role")
|
||||||
check(env.R("Moderator"):has_player(players.bob), "the role applies before confirmation")
|
check(env.R("Moderator"):has_player(players.bob), "the role applies before confirmation")
|
||||||
check(#env.events == 1 and eq(env.events[1].data.assigned, { 5 }), "the event carries the assigned role id")
|
check(#env.events == 1, "one event is raised")
|
||||||
|
eq(env.events[1].data.assigned, { 5 }, "the event carries the assigned role id")
|
||||||
check(env.events[1].data.by_player_index == 1, "the event carries who made the change")
|
check(env.events[1].data.by_player_index == 1, "the event carries who made the change")
|
||||||
check(#env.printed == 1 and env.printed[1][1] == "exp-roles.game-message-assign", "the change is announced")
|
check(#env.printed == 1 and env.printed[1][1] == "exp-roles.game-message-assign", "the change is announced")
|
||||||
check(#env.sounds == 1 and env.sounds[1] == "bob:utility/achievement_unlocked", "the assign sound plays")
|
eq(env.sounds, { "bob:utility/achievement_unlocked" }, "the assign sound plays")
|
||||||
|
|
||||||
local sd = env.Roles._script_data()
|
local sd = env.Roles._script_data()
|
||||||
check(eq(sd.local_players.bob, { 5 }) and eq(sd.pending.bob, { 5 }), "the role is held locally and pending")
|
eq(sd.local_players.bob, { 5 }, "the role is held locally")
|
||||||
|
eq(sd.pending.bob, { 5 }, "the role is pending")
|
||||||
|
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.R("Moderator"):assign(players.bob)
|
env.R("Moderator"):assign(players.bob)
|
||||||
check(#env.sent == 0 and #env.events == 0, "assigning a held role is a no-op")
|
check(#env.sent == 0 and #env.events == 0, "assigning a held role is a no-op")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("a confirmation releases the local hold", function(env)
|
test("receive_assignment_updates releases the local hold once confirmed", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Moderator"):assign(players.bob)
|
env.R("Moderator"):assign(players.bob)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.Roles.receive_assignment_updates{ Env.assignment("bob", { 6, 5 }) }
|
env.Roles.receive_assignment_updates{ env.assignment("bob", { 6, 5 }) }
|
||||||
check(#env.events == 0 and #env.printed == 0, "a confirmation raises nothing")
|
check(#env.events == 0 and #env.printed == 0, "a confirmation raises nothing")
|
||||||
|
|
||||||
local sd = env.Roles._script_data()
|
local sd = env.Roles._script_data()
|
||||||
@@ -49,17 +51,19 @@ test("a confirmation releases the local hold", function(env)
|
|||||||
check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation")
|
check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("unassign a synced role", function(env)
|
test("role:unassign removes a synced role", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Regular"):unassign(players.bob, { by_player_name = "alice", silent = true })
|
env.R("Regular"):unassign(players.bob, { by_player_name = "alice", silent = true })
|
||||||
check(#env.sent == 1 and eq(env.sent[1].data.unassign, { 6 }), "the unassignment is sent")
|
check(#env.sent == 1, "the unassignment is sent")
|
||||||
|
eq(env.sent[1].data.unassign, { 6 }, "the payload names the role")
|
||||||
check(not env.R("Regular"):has_player(players.bob), "the role is removed locally")
|
check(not env.R("Regular"):has_player(players.bob), "the role is removed locally")
|
||||||
check(#env.events == 1 and eq(env.events[1].data.unassigned, { 6 }), "the event carries the unassigned role id")
|
check(#env.events == 1, "one event is raised")
|
||||||
|
eq(env.events[1].data.unassigned, { 6 }, "the event carries the unassigned role id")
|
||||||
check(#env.printed == 0, "silent suppresses the announcement")
|
check(#env.printed == 0, "silent suppresses the announcement")
|
||||||
check(#env.sounds == 1 and env.sounds[1] == "bob:utility/game_lost", "the unassign sound plays")
|
eq(env.sounds, { "bob:utility/game_lost" }, "the unassign sound plays")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("a rejection rolls the assignment back", function(env)
|
test("reject_assignment rolls the assignment back", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Jail"):assign(players.alice)
|
env.R("Jail"):assign(players.alice)
|
||||||
check(env.R("Jail"):has_player(players.alice), "the role applies before rejection")
|
check(env.R("Jail"):has_player(players.alice), "the role applies before rejection")
|
||||||
@@ -68,52 +72,54 @@ test("a rejection rolls the assignment back", function(env)
|
|||||||
env.Roles.reject_assignment{ name = "alice", role_ids = { 7 } }
|
env.Roles.reject_assignment{ name = "alice", role_ids = { 7 } }
|
||||||
check(not env.R("Jail"):has_player(players.alice), "the rejected role is rolled back")
|
check(not env.R("Jail"):has_player(players.alice), "the rejected role is rolled back")
|
||||||
check(#env.sent == 0, "the rollback is not sent to the controller")
|
check(#env.sent == 0, "the rollback is not sent to the controller")
|
||||||
check(#env.events == 1 and eq(env.events[1].data.unassigned, { 7 }), "the rollback raises the event")
|
check(#env.events == 1, "the rollback raises the event")
|
||||||
|
eq(env.events[1].data.unassigned, { 7 }, "the event carries the refused role id")
|
||||||
|
|
||||||
local sd = env.Roles._script_data()
|
local sd = env.Roles._script_data()
|
||||||
check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state")
|
check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("local only roles never reach the controller", function(env)
|
test("role:assign with local_only never reaches the controller", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
||||||
check(#env.sent == 0, "a local only role is not sent")
|
check(#env.sent == 0, "the assignment is not sent")
|
||||||
check(env.R("Regular"):has_player(players.alice), "a local only role applies")
|
check(env.R("Regular"):has_player(players.alice), "the role applies")
|
||||||
|
|
||||||
local sd = env.Roles._script_data()
|
local sd = env.Roles._script_data()
|
||||||
check(sd.pending.alice == nil and eq(sd.local_players.alice, { 6 }), "a local only role is not pending")
|
check(sd.pending.alice == nil, "the role is not pending")
|
||||||
|
eq(sd.local_players.alice, { 6 }, "the role is held locally")
|
||||||
|
|
||||||
env.Roles.receive_assignment_updates{ Env.assignment("alice", { 5 }) }
|
env.Roles.receive_assignment_updates{ env.assignment("alice", { 5 }) }
|
||||||
check(env.R("Regular"):has_player(players.alice), "a local only role survives controller updates")
|
check(env.R("Regular"):has_player(players.alice), "the role survives controller updates")
|
||||||
|
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.R("Regular"):unassign(players.alice, { local_only = true })
|
env.R("Regular"):unassign(players.alice, { local_only = true })
|
||||||
check(not env.R("Regular"):has_player(players.alice) and #env.sent == 0, "removed without sync")
|
check(not env.R("Regular"):has_player(players.alice) and #env.sent == 0, "removed without sync")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("jail suppresses every other role", function(env)
|
test("role:assign of a higher priority role suppresses the rest", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Jail"):assign(players.alice, { by_player_name = "<server>", silent = true })
|
env.R("Jail"):assign(players.alice, { by_player_name = "<server>", silent = true })
|
||||||
check(eq(Test.names(env.Roles.get_player_roles(players.alice)), { "Jail" }), "only the jail role applies")
|
eq(env.names(env.Roles.get_player_roles(players.alice)), { "Jail" }, "only the jail role applies")
|
||||||
check(env.R("Moderator"):has_player(players.alice), "a suppressed role is still held")
|
check(env.R("Moderator"):has_player(players.alice), "a suppressed role is still held")
|
||||||
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.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_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")
|
check(not env.Roles.player_outranks(players.alice, players.bob), "a jailed player no longer outranks")
|
||||||
check(eq(env.events[1].data.assigned, { 7 }) and #env.events[1].data.unassigned == 0,
|
eq(env.events[1].data.assigned, { 7 }, "the event lists the jail role")
|
||||||
"the event lists only the jail role")
|
check(#env.events[1].data.unassigned == 0, "the suppressed roles are not listed as unassigned")
|
||||||
|
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.R("Jail"):unassign(players.alice, { by_player_name = "<server>", silent = true })
|
env.R("Jail"):unassign(players.alice, { by_player_name = "<server>", silent = true })
|
||||||
check(eq(Test.sorted(Test.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" }),
|
eq(Suite.sorted(env.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
|
||||||
"unjail restores the roles")
|
"unjail restores the roles")
|
||||||
check(#env.events[1].data.assigned == 0 and eq(env.events[1].data.unassigned, { 7 }),
|
check(#env.events[1].data.assigned == 0, "the restored roles are not listed as assigned")
|
||||||
"the unjail event lists only the jail role")
|
eq(env.events[1].data.unassigned, { 7 }, "the unjail event lists the jail role")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("the server can not be assigned roles", function(env)
|
test("role:assign ignores the server", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
env.R("Moderator"):assign(env.server)
|
env.R("Moderator"):assign(env.server)
|
||||||
check(#env.sent == 0 and #env.events == 0, "assigning to the server is a no-op")
|
check(#env.sent == 0 and #env.events == 0, "assigning to the server is a no-op")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return Env.finish()
|
return Suite.run()
|
||||||
|
|||||||
+33
-26
@@ -1,10 +1,11 @@
|
|||||||
--[[-- Test environment for module/control.lua
|
--[[-- Test environment for module/control.lua
|
||||||
Composes the shared stubs and framework from the repository test folder with
|
The module specific extension between the shared stubs and the test files: it
|
||||||
the fixtures for this plugin. Each call to Env.new() is an independent
|
creates environments which combine the stubs with a fresh copy of the roles
|
||||||
environment with a fresh copy of the roles module.
|
module and the fixtures below, and hands each test file a suite built around
|
||||||
|
that factory.
|
||||||
|
|
||||||
Run as a chunk by test/lua/runner.js, receiving the shared folder and
|
Run as a chunk by test/lua/runner.js, receiving the shared folder. The suite
|
||||||
returning this table, which is in turn passed to each test chunk.
|
returned here becomes `...` in each test file.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local shared_root = ... --- @type string
|
local shared_root = ... --- @type string
|
||||||
@@ -14,13 +15,8 @@ local plugin_root = assert(source:match("^@(.*)/test/lua/env%.lua$"))
|
|||||||
local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))()
|
local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))()
|
||||||
local Framework = assert(loadfile(shared_root .. "/framework.lua"))()
|
local Framework = assert(loadfile(shared_root .. "/framework.lua"))()
|
||||||
|
|
||||||
local Env = {
|
|
||||||
--- Test registry for the file being run, see framework.lua
|
|
||||||
Test = Framework.new(),
|
|
||||||
}
|
|
||||||
|
|
||||||
--- Standard fixture: permission names sent once and referenced by zero based index
|
--- Standard fixture: permission names sent once and referenced by zero based index
|
||||||
Env.permission_names = {
|
local permission_names = {
|
||||||
"core.admin", -- 0
|
"core.admin", -- 0
|
||||||
"exp_scenario.command.kill", -- 1
|
"exp_scenario.command.kill", -- 1
|
||||||
"exp_scenario.command.jail", -- 2
|
"exp_scenario.command.jail", -- 2
|
||||||
@@ -36,7 +32,7 @@ local function meta(order, extra)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Standard fixture: the roles as the controller would send them on initialise
|
--- Standard fixture: the roles as the controller would send them on initialise
|
||||||
function Env.role_records()
|
local function role_records()
|
||||||
return {
|
return {
|
||||||
{ id = 0, name = "Cluster Admin", permissions = { 0 }, meta = meta(1, { short_hand = "SYS" }) },
|
{ 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 = 5, name = "Moderator", permissions = { 1, 2, 3, 5 }, meta = meta(2, { color = { r = 0, g = 170, b = 0 } }) },
|
||||||
@@ -47,12 +43,8 @@ function Env.role_records()
|
|||||||
}
|
}
|
||||||
end
|
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
|
--- Create an independent environment with a fresh copy of the roles module
|
||||||
function Env.new()
|
local function make_env()
|
||||||
local env = Stubs.new()
|
local env = Stubs.new()
|
||||||
|
|
||||||
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))()
|
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))()
|
||||||
@@ -63,22 +55,37 @@ function Env.new()
|
|||||||
return (assert(env.Roles.get_role_by_name(name), "No role named " .. name))
|
return (assert(env.Roles.get_role_by_name(name), "No role named " .. name))
|
||||||
end
|
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
|
||||||
|
|
||||||
|
--- An assignment record as the controller would send it
|
||||||
|
function env.assignment(name, role_ids)
|
||||||
|
return { name = name, role_ids = role_ids }
|
||||||
|
end
|
||||||
|
|
||||||
--- Load the standard fixture and the given assignments
|
--- Load the standard fixture and the given assignments
|
||||||
function env.initialise(assignments)
|
function env.initialise(assignments)
|
||||||
env.Roles.initialise{
|
env.Roles.initialise{
|
||||||
permission_names = Env.permission_names,
|
permission_names = permission_names,
|
||||||
roles = Env.role_records(),
|
roles = role_records(),
|
||||||
assignments = assignments or {},
|
assignments = assignments or {},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Save and load the map, as far as the roles module can tell
|
||||||
|
local stub_save_load = env.save_load
|
||||||
|
function env.save_load()
|
||||||
|
stub_save_load()
|
||||||
|
env.Roles.on_load()
|
||||||
|
end
|
||||||
|
|
||||||
return env
|
return env
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Run the declared tests, each against a fresh environment
|
return Framework.new_suite(make_env)
|
||||||
function Env.finish()
|
|
||||||
Env.Test.run(Env.new)
|
|
||||||
return Env.Test.results_json()
|
|
||||||
end
|
|
||||||
|
|
||||||
return Env
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
--- Listing and printing to the players who hold a role
|
--- Tests for listing and printing to the players who hold a role
|
||||||
local Env = ...
|
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||||
local Test = Env.Test
|
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||||
local test, check, eq = Test.test, Test.check, Test.eq
|
|
||||||
|
|
||||||
--- alice and dave are moderators with dave offline, zed has never joined
|
--- alice and dave are moderators with dave offline, zed has never joined
|
||||||
local function setup(env)
|
local function setup(env)
|
||||||
@@ -11,47 +10,47 @@ local function setup(env)
|
|||||||
dave = env.add_player("dave", 4, false),
|
dave = env.add_player("dave", 4, false),
|
||||||
}
|
}
|
||||||
env.initialise{
|
env.initialise{
|
||||||
Env.assignment("alice", { 5 }),
|
env.assignment("alice", { 5 }),
|
||||||
Env.assignment("bob", { 6 }),
|
env.assignment("bob", { 6 }),
|
||||||
Env.assignment("dave", { 5 }),
|
env.assignment("dave", { 5 }),
|
||||||
Env.assignment("zed", { 5 }),
|
env.assignment("zed", { 5 }),
|
||||||
}
|
}
|
||||||
env.Roles.set_emit_events(true)
|
env.Roles.set_emit_events(true)
|
||||||
return players
|
return players
|
||||||
end
|
end
|
||||||
|
|
||||||
test("player names include players not on this map", function(env)
|
test("role:get_player_names includes players not on this map", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
check(eq(Test.sorted(env.R("Moderator"):get_player_names()), { "alice", "dave", "zed" }),
|
eq(Suite.sorted(env.R("Moderator"):get_player_names()), { "alice", "dave", "zed" },
|
||||||
"every player given the role is listed")
|
"every player given the role is listed")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("players are limited to this map and can be filtered", function(env)
|
test("role:get_player_names counts a player in both lists once", function(env)
|
||||||
setup(env)
|
|
||||||
local mod = env.R("Moderator")
|
|
||||||
check(eq(Test.sorted(Test.names(mod:get_players())), { "alice", "dave" }), "players are limited to this map")
|
|
||||||
check(eq(Test.names(mod:get_players(true)), { "alice" }), "filtered to connected players")
|
|
||||||
check(eq(Test.names(mod:get_players(false)), { "dave" }), "filtered to offline players")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("a player holding the role in both lists is counted once", function(env)
|
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
||||||
env.Roles.receive_assignment_updates{ Env.assignment("alice", { 5, 6 }) }
|
env.Roles.receive_assignment_updates{ env.assignment("alice", { 5, 6 }) }
|
||||||
|
|
||||||
local sd = env.Roles._script_data()
|
local sd = env.Roles._script_data()
|
||||||
check(sd.local_players.alice ~= nil and sd.synced_players.alice ~= nil, "the role is held in both lists")
|
check(sd.local_players.alice ~= nil and sd.synced_players.alice ~= nil, "the role is held in both lists")
|
||||||
check(eq(Test.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }), "the player is counted once")
|
eq(Suite.sorted(env.R("Regular"):get_player_names()), { "alice", "bob" }, "the player is counted once")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("print reaches online holders", function(env)
|
test("role:get_players is limited to this map and can be filtered", function(env)
|
||||||
|
setup(env)
|
||||||
|
local mod = env.R("Moderator")
|
||||||
|
eq(Suite.sorted(env.names(mod:get_players())), { "alice", "dave" }, "players are limited to this map")
|
||||||
|
eq(env.names(mod:get_players(true)), { "alice" }, "filtered to connected players")
|
||||||
|
eq(env.names(mod:get_players(false)), { "dave" }, "filtered to offline players")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("role:print reaches online holders", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
check(env.R("Moderator"):print("hello") == 1, "print returns the number of players reached")
|
check(env.R("Moderator"):print("hello") == 1, "print returns the number of players reached")
|
||||||
check(#env.printed == 1 and env.printed[1].to == "alice", "only online holders are reached")
|
check(#env.printed == 1 and env.printed[1].to == "alice", "only online holders are reached")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("printing to the higher roles", function(env)
|
test("role:print over get_higher_roles can repeat", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
@@ -62,15 +61,7 @@ test("printing to the higher roles", function(env)
|
|||||||
local got = {}
|
local got = {}
|
||||||
for _, entry in ipairs(env.printed) do got[#got + 1] = entry.to end
|
for _, entry in ipairs(env.printed) do got[#got + 1] = entry.to end
|
||||||
-- alice holds two of the roles, so like the legacy system the message can repeat
|
-- alice holds two of the roles, so like the legacy system the message can repeat
|
||||||
check(eq(Test.sorted(got), { "alice", "alice", "bob" }), "the online holders of each role are reached")
|
eq(Suite.sorted(got), { "alice", "alice", "bob" }, "the online holders of each role are reached")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("deep equality helper", function(env)
|
return Suite.run()
|
||||||
check(Test.deep_eq({ a = { 1, 2 }, b = "x" }, { a = { 1, 2 }, b = "x" }), "equal nested tables")
|
|
||||||
check(not Test.deep_eq({ a = { 1, 2 } }, { a = { 1, 3 } }), "differing nested values")
|
|
||||||
check(not Test.deep_eq({ a = 1 }, { a = 1, b = 2 }), "extra keys on the right")
|
|
||||||
check(not Test.deep_eq({ a = 1, b = 2 }, { a = 1 }), "extra keys on the left")
|
|
||||||
check(Test.deep_eq(env.Roles._script_data().pending, {}), "works against module state")
|
|
||||||
end)
|
|
||||||
|
|
||||||
return Env.finish()
|
|
||||||
|
|||||||
@@ -1,28 +1,58 @@
|
|||||||
--- Role lookup, decoding, and comparisons
|
--- Tests for the role lookup functions of module/control.lua
|
||||||
local Env = ...
|
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||||
local Test = Env.Test
|
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||||
local test, check, eq = Test.test, Test.check, Test.eq
|
|
||||||
|
|
||||||
test("roles are ordered most privileged first", function(env)
|
test("get_role returns the role with the given clusterio id", function(env)
|
||||||
env.initialise{}
|
env.initialise{}
|
||||||
check(eq(Test.names(env.Roles.get_ordered_roles()), { "Cluster Admin", "Moderator", "Regular", "Jail", "Player" }),
|
check(env.Roles.get_role(6).name == "Regular", "the role is found by id")
|
||||||
"ordered roles follow their order with deleted roles skipped")
|
|
||||||
check(#env.Roles.get_roles() == 5, "get_roles returns every role")
|
|
||||||
|
|
||||||
local sorted = env.Roles.sort_roles{ env.R("Player"), env.R("Cluster Admin"), env.R("Regular") }
|
|
||||||
check(eq(Test.names(sorted), { "Cluster Admin", "Regular", "Player" }), "sort_roles orders most privileged first")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("roles are looked up by id", function(env)
|
|
||||||
env.initialise{}
|
|
||||||
check(env.Roles.get_role(6).name == "Regular", "get_role looks up by clusterio id")
|
|
||||||
check(env.Roles.get_role_by_name("Moderator").id == 5, "get_role_by_name searches the roles")
|
|
||||||
check(env.Roles.get_role(env.R("Jail").id) == env.R("Jail"), "lookups return the same object")
|
check(env.Roles.get_role(env.R("Jail").id) == env.R("Jail"), "lookups return the same object")
|
||||||
check(env.Roles.get_role(9) == nil and env.Roles.get_role_by_name("Gone") == nil, "deleted roles are not known")
|
check(env.Roles.get_role(9) == nil, "deleted roles are not known")
|
||||||
check(env.Roles.get_default_role() == env.R("Player"), "the default role comes from is_default")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("role records are decoded", function(env)
|
test("get_role_by_name searches the roles", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
check(env.Roles.get_role_by_name("Moderator").id == 5, "the role is found by name")
|
||||||
|
check(env.Roles.get_role_by_name("Gone") == nil, "deleted roles are not known")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("get_roles returns every role", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
check(#env.Roles.get_roles() == 5, "every role is returned with deleted roles skipped")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("get_ordered_roles returns the most privileged first", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
eq(env.names(env.Roles.get_ordered_roles()), { "Cluster Admin", "Moderator", "Regular", "Jail", "Player" },
|
||||||
|
"roles follow their order")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("sort_roles orders most privileged first", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
local sorted = env.Roles.sort_roles{ env.R("Player"), env.R("Cluster Admin"), env.R("Regular") }
|
||||||
|
eq(env.names(sorted), { "Cluster Admin", "Regular", "Player" }, "the list is sorted in place")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("get_default_role follows is_default", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
check(env.Roles.get_default_role() == env.R("Player"), "the default role is known")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("get_higher_roles and get_lower_roles include the role and exclude the default", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
eq(Suite.sorted(env.names(env.Roles.get_higher_roles(env.R("Regular")))),
|
||||||
|
{ "Cluster Admin", "Moderator", "Regular" }, "higher roles")
|
||||||
|
eq(Suite.sorted(env.names(env.Roles.get_lower_roles(env.R("Regular")))),
|
||||||
|
{ "Jail", "Regular" }, "lower roles")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("role:is_higher_than and role:is_lower_than compare on order", function(env)
|
||||||
|
env.initialise{}
|
||||||
|
check(env.R("Moderator"):is_higher_than(env.R("Player")), "a lower order is more privileged")
|
||||||
|
check(env.R("Player"):is_lower_than(env.R("Moderator")), "is_lower_than is the reverse")
|
||||||
|
check(not env.R("Moderator"):is_higher_than(env.R("Moderator")), "a role is not higher than itself")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("initialise decodes the role records", function(env)
|
||||||
env.initialise{}
|
env.initialise{}
|
||||||
check(env.R("Cluster Admin").short_hand == "SYS", "short hand decoded")
|
check(env.R("Cluster Admin").short_hand == "SYS", "short hand decoded")
|
||||||
check(env.R("Moderator").short_hand == "Moderator", "short hand falls back to the name")
|
check(env.R("Moderator").short_hand == "Moderator", "short hand falls back to the name")
|
||||||
@@ -30,19 +60,4 @@ test("role records are decoded", function(env)
|
|||||||
check(env.R("Jail").priority == 1 and env.R("Jail").block_auto_assign, "priority and block auto assign decoded")
|
check(env.R("Jail").priority == 1 and env.R("Jail").block_auto_assign, "priority and block auto assign decoded")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("roles compare on their order", function(env)
|
return Suite.run()
|
||||||
env.initialise{}
|
|
||||||
check(env.R("Moderator"):is_higher_than(env.R("Player")), "a lower order is more privileged")
|
|
||||||
check(env.R("Player"):is_lower_than(env.R("Moderator")), "is_lower_than is the reverse")
|
|
||||||
check(not env.R("Moderator"):is_higher_than(env.R("Moderator")), "a role is not higher than itself")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("higher and lower roles", function(env)
|
|
||||||
env.initialise{}
|
|
||||||
check(eq(Test.sorted(Test.names(env.Roles.get_higher_roles(env.R("Regular")))), { "Cluster Admin", "Moderator", "Regular" }),
|
|
||||||
"higher roles include the role itself and exclude the default role")
|
|
||||||
check(eq(Test.sorted(Test.names(env.Roles.get_lower_roles(env.R("Regular")))), { "Jail", "Regular" }),
|
|
||||||
"lower roles include the role itself and exclude the default role")
|
|
||||||
end)
|
|
||||||
|
|
||||||
return Env.finish()
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
--- Player role and permission checks
|
--- Tests for the player checks of module/control.lua
|
||||||
local Env = ...
|
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||||
local Test = Env.Test
|
local test, check, eq = Suite.test, Suite.check, Suite.eq
|
||||||
local test, check, eq = Test.test, Test.check, Test.eq
|
|
||||||
|
|
||||||
--- alice is a moderator, bob is a regular, and carol has only the default role
|
--- alice is a moderator, bob is a regular, and carol has only the default role
|
||||||
local function setup(env)
|
local function setup(env)
|
||||||
@@ -11,42 +10,42 @@ local function setup(env)
|
|||||||
carol = env.add_player("carol", 3),
|
carol = env.add_player("carol", 3),
|
||||||
}
|
}
|
||||||
env.initialise{
|
env.initialise{
|
||||||
Env.assignment("alice", { 5 }),
|
env.assignment("alice", { 5 }),
|
||||||
Env.assignment("bob", { 6 }),
|
env.assignment("bob", { 6 }),
|
||||||
}
|
}
|
||||||
return players
|
return players
|
||||||
end
|
end
|
||||||
|
|
||||||
test("player roles include the default role", function(env)
|
test("get_player_roles includes the default role", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
check(eq(Test.sorted(Test.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" }),
|
eq(Suite.sorted(env.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
|
||||||
"held roles and the default role")
|
"held roles and the default role")
|
||||||
check(eq(Test.names(env.Roles.get_player_roles(players.carol)), { "Player" }),
|
eq(env.names(env.Roles.get_player_roles(players.carol)), { "Player" },
|
||||||
"a player with no roles has the default role")
|
"a player with no roles has the default role")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("the server is nil or a player with index 0", function(env)
|
test("get_player_roles treats nil and index 0 as the server", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
check(eq(Test.names(env.Roles.get_player_roles(nil)), { "<server>" }), "nil is the server")
|
eq(env.names(env.Roles.get_player_roles(nil)), { "<server>" }, "nil is the server")
|
||||||
check(eq(Test.names(env.Roles.get_player_roles(env.server)), { "<server>" }), "index 0 is the server")
|
eq(env.names(env.Roles.get_player_roles(env.server)), { "<server>" }, "index 0 is the server")
|
||||||
check(env.Roles.player_has_permission(nil, "anything.at.all"), "the server has every permission")
|
check(env.Roles.player_has_permission(nil, "anything.at.all"), "the server has every permission")
|
||||||
check(not env.R("Player"):has_player(nil), "the server does not have the default role")
|
check(not env.R("Player"):has_player(nil), "the server does not have the default role")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("the highest role", function(env)
|
test("get_player_highest_role", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
check(env.Roles.get_player_highest_role(players.alice) == env.R("Moderator"), "highest held role")
|
check(env.Roles.get_player_highest_role(players.alice) == env.R("Moderator"), "the highest held role")
|
||||||
check(env.Roles.get_player_highest_role(players.carol) == env.R("Player"), "the default role when none are held")
|
check(env.Roles.get_player_highest_role(players.carol) == env.R("Player"), "the default role when none are held")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("permission checks", function(env)
|
test("player_has_permission", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "granted by a held role")
|
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "granted by a held role")
|
||||||
check(env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "granted by the default role")
|
check(env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "granted by the default role")
|
||||||
check(not env.Roles.player_has_permission(players.bob, "exp_scenario.command.jail"), "not granted")
|
check(not env.Roles.player_has_permission(players.bob, "exp_scenario.command.jail"), "not granted")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("has any and has all", function(env)
|
test("player_has_any_permission and player_has_all_permission", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
local jail, kill = "exp_scenario.command.jail", "exp_scenario.command.kill"
|
local jail, kill = "exp_scenario.command.jail", "exp_scenario.command.kill"
|
||||||
check(env.Roles.player_has_any_permission(players.bob, jail, kill), "any passes when one is granted")
|
check(env.Roles.player_has_any_permission(players.bob, jail, kill), "any passes when one is granted")
|
||||||
@@ -59,17 +58,21 @@ test("has any and has all", function(env)
|
|||||||
check(env.Roles.player_has_any_permission(nil, "anything.at.all"), "the server passes any")
|
check(env.Roles.player_has_any_permission(nil, "anything.at.all"), "the server passes any")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("role permission and player methods", function(env)
|
test("role:has_permission", function(env)
|
||||||
local players = setup(env)
|
setup(env)
|
||||||
check(env.R("Moderator"):has_permission("exp_scenario.command.kill"), "has_permission granted")
|
check(env.R("Moderator"):has_permission("exp_scenario.command.kill"), "granted")
|
||||||
check(not env.R("Regular"):has_permission("exp_scenario.command.jail"), "has_permission not granted")
|
check(not env.R("Regular"):has_permission("exp_scenario.command.jail"), "not granted")
|
||||||
check(env.R("Cluster Admin"):has_permission("anything.at.all"), "core.admin grants everything")
|
check(env.R("Cluster Admin"):has_permission("anything.at.all"), "core.admin grants everything")
|
||||||
check(env.R("Moderator"):has_player(players.alice), "has_player for a held role")
|
end)
|
||||||
check(not env.R("Moderator"):has_player(players.bob), "has_player for a role not held")
|
|
||||||
|
test("role:has_player", function(env)
|
||||||
|
local players = setup(env)
|
||||||
|
check(env.R("Moderator"):has_player(players.alice), "a held role")
|
||||||
|
check(not env.R("Moderator"):has_player(players.bob), "a role not held")
|
||||||
check(env.R("Player"):has_player(players.carol), "everyone has the default role")
|
check(env.R("Player"):has_player(players.carol), "everyone has the default role")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("outranks", function(env)
|
test("player_outranks", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
check(env.Roles.player_outranks(players.alice, players.bob), "a higher role outranks a lower one")
|
check(env.Roles.player_outranks(players.alice, players.bob), "a higher role outranks a lower one")
|
||||||
check(not env.Roles.player_outranks(players.bob, players.alice), "a lower role does not outrank a higher one")
|
check(not env.Roles.player_outranks(players.bob, players.alice), "a lower role does not outrank a higher one")
|
||||||
@@ -78,4 +81,4 @@ test("outranks", function(env)
|
|||||||
check(not env.Roles.player_outranks(players.alice, nil), "nobody outranks the server")
|
check(not env.Roles.player_outranks(players.alice, nil), "nobody outranks the server")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return Env.finish()
|
return Suite.run()
|
||||||
|
|||||||
+46
-39
@@ -1,7 +1,6 @@
|
|||||||
--- Initialise semantics and updates from the controller
|
--- Tests for the sync entry points of module/control.lua
|
||||||
local Env = ...
|
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
|
||||||
local Test = Env.Test
|
local test, check, eq, deep_eq = Suite.test, Suite.check, Suite.eq, Suite.deep_eq
|
||||||
local test, check, eq = Test.test, Test.check, Test.eq
|
|
||||||
|
|
||||||
--- alice is a moderator and carol has only the default role, both connected
|
--- alice is a moderator and carol has only the default role, both connected
|
||||||
local function setup(env)
|
local function setup(env)
|
||||||
@@ -14,8 +13,8 @@ local function setup(env)
|
|||||||
env.admin_state[player.name] = state
|
env.admin_state[player.name] = state
|
||||||
end)
|
end)
|
||||||
env.initialise{
|
env.initialise{
|
||||||
Env.assignment("alice", { 5 }),
|
env.assignment("alice", { 5 }),
|
||||||
Env.assignment("zed", { 5, 6 }), -- has never joined this map
|
env.assignment("zed", { 5, 6 }), -- has never joined this map
|
||||||
}
|
}
|
||||||
env.Roles.set_emit_events(true)
|
env.Roles.set_emit_events(true)
|
||||||
return players
|
return players
|
||||||
@@ -23,19 +22,35 @@ end
|
|||||||
|
|
||||||
test("initialise applies triggers and raises empty events", function(env)
|
test("initialise applies triggers and raises empty events", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
check(env.admin_state.alice == true and env.admin_state.carol == false,
|
deep_eq(env.admin_state, { alice = true, carol = false }, "triggers run for connected players")
|
||||||
"triggers run for connected players")
|
|
||||||
check(#env.events == 2, "the event is raised once per connected player")
|
check(#env.events == 2, "the event is raised once per connected player")
|
||||||
check(#env.events[1].data.assigned == 0 and #env.events[1].data.unassigned == 0, "the events are empty")
|
check(#env.events[1].data.assigned == 0 and #env.events[1].data.unassigned == 0, "the events are empty")
|
||||||
check(#env.printed == 0 and #env.sent == 0 and #env.sounds == 0, "initialise is silent and sends nothing")
|
check(#env.printed == 0 and #env.sent == 0 and #env.sounds == 0, "initialise is silent and sends nothing")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("controller assignments apply and are announced", function(env)
|
test("initialise is authoritative for pending roles", function(env)
|
||||||
|
local players = setup(env)
|
||||||
|
env.R("Moderator"):assign(players.carol)
|
||||||
|
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
|
||||||
|
|
||||||
|
local sd = env.Roles._script_data()
|
||||||
|
eq(sd.pending.carol, { 5 }, "the role is pending before initialise")
|
||||||
|
|
||||||
|
env.reset_log()
|
||||||
|
env.initialise{ env.assignment("alice", { 5 }) }
|
||||||
|
check(sd.pending.carol == nil, "pending roles are cleared")
|
||||||
|
check(not env.R("Moderator"):has_player(players.carol), "an unconfirmed role is given up")
|
||||||
|
eq(sd.local_players.carol, { 7 }, "local only roles are kept")
|
||||||
|
check(#env.sent == 0, "initialise sends nothing")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("receive_assignment_updates applies controller changes", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.Roles.receive_assignment_updates{ Env.assignment("carol", { 5 }) }
|
env.Roles.receive_assignment_updates{ env.assignment("carol", { 5 }) }
|
||||||
check(env.R("Moderator"):has_player(players.carol), "the assignment applies")
|
check(env.R("Moderator"):has_player(players.carol), "the assignment applies")
|
||||||
check(#env.events == 1 and eq(env.events[1].data.assigned, { 5 }), "the event is raised")
|
check(#env.events == 1, "the event is raised")
|
||||||
|
eq(env.events[1].data.assigned, { 5 }, "the event carries the role id")
|
||||||
check(env.events[1].data.by_player_index == 0, "controller changes have no by player")
|
check(env.events[1].data.by_player_index == 0, "controller changes have no by player")
|
||||||
check(#env.printed == 1 and env.printed[1][4] == "<server>", "the change is announced by the server")
|
check(#env.printed == 1 and env.printed[1][4] == "<server>", "the change is announced by the server")
|
||||||
check(env.admin_state.carol == true, "triggers follow the assignment")
|
check(env.admin_state.carol == true, "triggers follow the assignment")
|
||||||
@@ -43,18 +58,18 @@ test("controller assignments apply and are announced", function(env)
|
|||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.Roles.receive_assignment_updates{ { name = "carol", role_ids = {}, is_deleted = true } }
|
env.Roles.receive_assignment_updates{ { name = "carol", role_ids = {}, is_deleted = true } }
|
||||||
check(not env.R("Moderator"):has_player(players.carol), "a removal applies")
|
check(not env.R("Moderator"):has_player(players.carol), "a removal applies")
|
||||||
check(#env.events == 1 and eq(env.events[1].data.unassigned, { 5 }), "the removal raises the event")
|
eq(env.events[1].data.unassigned, { 5 }, "the removal raises the event")
|
||||||
check(env.admin_state.carol == false, "triggers follow the removal")
|
check(env.admin_state.carol == false, "triggers follow the removal")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("changes for players not on this map raise nothing", function(env)
|
test("receive_assignment_updates for players not on this map raises nothing", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.Roles.receive_assignment_updates{ Env.assignment("zed", { 5 }) }
|
env.Roles.receive_assignment_updates{ env.assignment("zed", { 5 }) }
|
||||||
check(#env.events == 0 and #env.printed == 0, "no event and no announcement")
|
check(#env.events == 0 and #env.printed == 0, "no event and no announcement")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("role updates apply to their holders", function(env)
|
test("receive_role_updates applies to the holders", function(env)
|
||||||
setup(env)
|
setup(env)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
env.Roles.receive_role_updates{
|
env.Roles.receive_role_updates{
|
||||||
@@ -70,33 +85,17 @@ test("role updates apply to their holders", function(env)
|
|||||||
check(env.Roles.get_role(6) == nil, "a deleted role is removed")
|
check(env.Roles.get_role(6) == nil, "a deleted role is removed")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("the default role follows is_default", function(env)
|
test("receive_role_updates moves the default role", function(env)
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.Roles.receive_role_updates{
|
env.Roles.receive_role_updates{
|
||||||
{ id = 1, name = "Player", permissions = {}, meta = { id = 0, order = 5 } },
|
{ id = 1, name = "Player", permissions = {}, meta = { id = 0, order = 5 } },
|
||||||
{ id = 8, name = "Guest", permissions = {}, meta = { id = 0, order = 7 }, is_default = true },
|
{ id = 8, name = "Guest", permissions = {}, meta = { id = 0, order = 7 }, is_default = true },
|
||||||
}
|
}
|
||||||
check(env.Roles.get_default_role() == env.R("Guest"), "the new default role is known")
|
check(env.Roles.get_default_role() == env.R("Guest"), "the new default role is known")
|
||||||
check(eq(Test.names(env.Roles.get_player_roles(players.carol)), { "Guest" }), "players pick up the new default")
|
eq(env.names(env.Roles.get_player_roles(players.carol)), { "Guest" }, "players pick up the new default")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("initialise is authoritative for pending roles", function(env)
|
test("set_emit_events gates what is sent", function(env)
|
||||||
local players = setup(env)
|
|
||||||
env.R("Moderator"):assign(players.carol)
|
|
||||||
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
|
|
||||||
|
|
||||||
local sd = env.Roles._script_data()
|
|
||||||
check(eq(sd.pending.carol, { 5 }), "the role is pending before initialise")
|
|
||||||
|
|
||||||
env.reset_log()
|
|
||||||
env.initialise{ Env.assignment("alice", { 5 }) }
|
|
||||||
check(sd.pending.carol == nil, "pending roles are cleared")
|
|
||||||
check(not env.R("Moderator"):has_player(players.carol), "an unconfirmed role is given up")
|
|
||||||
check(eq(sd.local_players.carol, { 7 }), "local only roles are kept")
|
|
||||||
check(#env.sent == 0, "initialise sends nothing")
|
|
||||||
end)
|
|
||||||
|
|
||||||
test("nothing is sent while emit is disabled", function(env)
|
|
||||||
local players = setup(env)
|
local players = setup(env)
|
||||||
env.Roles.set_emit_events(false)
|
env.Roles.set_emit_events(false)
|
||||||
env.reset_log()
|
env.reset_log()
|
||||||
@@ -105,14 +104,22 @@ test("nothing is sent while emit is disabled", function(env)
|
|||||||
check(env.R("Regular"):has_player(players.alice), "the assignment still applies locally")
|
check(env.R("Regular"):has_player(players.alice), "the assignment still applies locally")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("roles are usable after load and triggers run on join", function(env)
|
test("on_load restores the state after a save and load", function(env)
|
||||||
setup(env)
|
local players = setup(env)
|
||||||
env.Roles.on_load()
|
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
|
||||||
check(env.R("Moderator"):is_higher_than(env.R("Regular")), "roles are usable after load")
|
env.save_load()
|
||||||
|
|
||||||
|
check(env.R("Moderator"):is_higher_than(env.R("Regular")), "role methods survive through the registered metatable")
|
||||||
|
check(env.R("Moderator"):has_player(players.alice), "synced roles survive")
|
||||||
|
check(env.R("Jail"):has_player(players.carol), "local roles survive")
|
||||||
|
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permission checks still answer")
|
||||||
|
end)
|
||||||
|
|
||||||
|
test("on_player_joined_game runs the triggers", function(env)
|
||||||
|
setup(env)
|
||||||
env.admin_state.alice = nil
|
env.admin_state.alice = nil
|
||||||
env.Roles.on_player_joined_game{ player_index = 1 }
|
env.Roles.on_player_joined_game{ player_index = 1 }
|
||||||
check(env.admin_state.alice ~= nil, "triggers run when a player joins")
|
check(env.admin_state.alice ~= nil, "triggers run when a player joins")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return Env.finish()
|
return Suite.run()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const fullMeta = new messages.RoleMetaRecord(
|
|||||||
7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false,
|
7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false,
|
||||||
);
|
);
|
||||||
|
|
||||||
t.test("RoleColor", subtest => {
|
t.test("class RoleColor", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.RoleColor, testMatrix(
|
testRoundTripJsonSerialisable(messages.RoleColor, testMatrix(
|
||||||
[0, 255], // r
|
[0, 255], // r
|
||||||
[0, 128], // g
|
[0, 128], // g
|
||||||
@@ -17,7 +17,7 @@ t.test("RoleColor", subtest => {
|
|||||||
subtest.end();
|
subtest.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("RoleMetaRecord", subtest => {
|
t.test("class RoleMetaRecord", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.RoleMetaRecord, testMatrix(
|
testRoundTripJsonSerialisable(messages.RoleMetaRecord, testMatrix(
|
||||||
[7], // id
|
[7], // id
|
||||||
[3], // order
|
[3], // order
|
||||||
@@ -34,7 +34,7 @@ t.test("RoleMetaRecord", subtest => {
|
|||||||
subtest.end();
|
subtest.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("RoleRecord", subtest => {
|
t.test("class RoleRecord", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.RoleRecord, testMatrix(
|
testRoundTripJsonSerialisable(messages.RoleRecord, testMatrix(
|
||||||
[7], // id
|
[7], // id
|
||||||
["Moderator"], // name
|
["Moderator"], // name
|
||||||
@@ -48,7 +48,7 @@ t.test("RoleRecord", subtest => {
|
|||||||
subtest.end();
|
subtest.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("AssignmentRecord", subtest => {
|
t.test("class AssignmentRecord", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.AssignmentRecord, testMatrix(
|
testRoundTripJsonSerialisable(messages.AssignmentRecord, testMatrix(
|
||||||
["alice"], // name
|
["alice"], // name
|
||||||
[new Set(), new Set([5, 6])], // roleIds
|
[new Set(), new Set([5, 6])], // roleIds
|
||||||
@@ -59,19 +59,34 @@ t.test("AssignmentRecord", subtest => {
|
|||||||
subtest.end();
|
subtest.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("update events and requests", subtest => {
|
const sampleRecord = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345);
|
||||||
const record = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345);
|
const sampleAssignment = new messages.AssignmentRecord("alice", new Set([5]), 12345);
|
||||||
const assignment = new messages.AssignmentRecord("alice", new Set([5]), 12345);
|
|
||||||
|
|
||||||
|
t.test("class RoleUpdatedEvent", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.RoleUpdatedEvent, testMatrix(
|
testRoundTripJsonSerialisable(messages.RoleUpdatedEvent, testMatrix(
|
||||||
[[], [record]], // updates
|
[[], [sampleRecord]], // updates
|
||||||
));
|
));
|
||||||
|
subtest.pass("round trips");
|
||||||
|
subtest.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test("class AssignmentUpdatedEvent", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.AssignmentUpdatedEvent, testMatrix(
|
testRoundTripJsonSerialisable(messages.AssignmentUpdatedEvent, testMatrix(
|
||||||
[[], [assignment]], // updates
|
[[], [sampleAssignment]], // updates
|
||||||
));
|
));
|
||||||
|
subtest.pass("round trips");
|
||||||
|
subtest.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test("class RoleMetaUpdateRequest", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.RoleMetaUpdateRequest, testMatrix(
|
testRoundTripJsonSerialisable(messages.RoleMetaUpdateRequest, testMatrix(
|
||||||
[new messages.RoleMetaRecord(7, 3), fullMeta], // meta
|
[new messages.RoleMetaRecord(7, 3), fullMeta], // meta
|
||||||
));
|
));
|
||||||
|
subtest.pass("round trips");
|
||||||
|
subtest.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test("class AssignmentUpdateRequest", subtest => {
|
||||||
testRoundTripJsonSerialisable(messages.AssignmentUpdateRequest, testMatrix(
|
testRoundTripJsonSerialisable(messages.AssignmentUpdateRequest, testMatrix(
|
||||||
["alice"], // name
|
["alice"], // name
|
||||||
[[], [5]], // assign
|
[[], [5]], // assign
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
|
|||||||
// Importing this defines the exp_scenario permissions the seed grants
|
// Importing this defines the exp_scenario permissions the seed grants
|
||||||
require("@expcluster/scenario/dist/node/permissions");
|
require("@expcluster/scenario/dist/node/permissions");
|
||||||
|
|
||||||
t.test("every seed permission is defined", subtest => {
|
t.test("seedRoles grant only defined permissions", subtest => {
|
||||||
for (const role of seedRoles) {
|
for (const role of seedRoles) {
|
||||||
for (const permission of flattenSeedPermissions(role)) {
|
for (const permission of flattenSeedPermissions(role)) {
|
||||||
subtest.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
|
subtest.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
|
||||||
@@ -15,7 +15,7 @@ t.test("every seed permission is defined", subtest => {
|
|||||||
subtest.end();
|
subtest.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("parents exist and their permissions are inherited", subtest => {
|
t.test("flattenSeedPermissions carries parents into their children", subtest => {
|
||||||
const byName = new Map(seedRoles.map(role => [role.name, role]));
|
const byName = new Map(seedRoles.map(role => [role.name, role]));
|
||||||
for (const role of seedRoles) {
|
for (const role of seedRoles) {
|
||||||
if (role.parent === undefined) {
|
if (role.parent === undefined) {
|
||||||
@@ -31,7 +31,7 @@ t.test("parents exist and their permissions are inherited", subtest => {
|
|||||||
subtest.end();
|
subtest.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
t.test("seed role names are unique with one default and one admin", subtest => {
|
t.test("seedRoles are unique with one default and one admin", subtest => {
|
||||||
const names = seedRoles.map(role => role.name);
|
const names = seedRoles.map(role => role.name);
|
||||||
subtest.strictSame(names.length, new Set(names).size, "names are unique");
|
subtest.strictSame(names.length, new Set(names).size, "names are unique");
|
||||||
subtest.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
|
subtest.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
|
||||||
|
|||||||
+73
-41
@@ -1,15 +1,30 @@
|
|||||||
--[[-- Test framework for plugin module tests
|
--[[-- Test framework for plugin module tests
|
||||||
Test files declare named tests with `Test.test(name, function(env) ... end)`.
|
A suite is a collection of named tests. Test files declare them with
|
||||||
The runner creates a fresh environment for every test, so tests are
|
`Suite.test(name, function(env) ... end)`, naming each test after the function
|
||||||
independent and can not leak state into each other.
|
or method it covers. Every test function receives a fresh environment from the
|
||||||
|
factory the suite was created with, so tests are independent and can not leak
|
||||||
|
state into each other.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local Framework = {}
|
local Framework = {}
|
||||||
|
|
||||||
--- Create a test registry, one is made per test file
|
--- Turn a value into a string for failure messages
|
||||||
function Framework.new()
|
local function repr(value, depth)
|
||||||
--- @class Test
|
if type(value) ~= "table" then return tostring(value) end
|
||||||
local Test = {
|
if depth > 3 then return "{...}" end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
for key, entry in pairs(value) do
|
||||||
|
parts[#parts + 1] = tostring(key) .. " = " .. repr(entry, depth + 1)
|
||||||
|
end
|
||||||
|
return "{ " .. table.concat(parts, ", ") .. " }"
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Create a suite of tests, one is made per test file
|
||||||
|
--- @param make_env fun(): table Creates the environment given to each test
|
||||||
|
function Framework.new_suite(make_env)
|
||||||
|
--- @class Suite
|
||||||
|
local Suite = {
|
||||||
tests = {}, -- { name, fn } in declaration order
|
tests = {}, -- { name, fn } in declaration order
|
||||||
results = {}, -- { name, ok, detail? } accumulated across tests
|
results = {}, -- { name, ok, detail? } accumulated across tests
|
||||||
}
|
}
|
||||||
@@ -19,37 +34,61 @@ function Framework.new()
|
|||||||
--- Declare a named test, its function receives a fresh environment
|
--- Declare a named test, its function receives a fresh environment
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param fn fun(env: table)
|
--- @param fn fun(env: table)
|
||||||
function Test.test(name, fn)
|
function Suite.test(name, fn)
|
||||||
Test.tests[#Test.tests + 1] = { name = name, fn = fn }
|
Suite.tests[#Suite.tests + 1] = { name = name, fn = fn }
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Record one check within the current test
|
--- Record a passing check within the current test
|
||||||
--- @param ok any Truthy when the check passed
|
--- @param name string
|
||||||
|
function Suite.pass(name)
|
||||||
|
Suite.results[#Suite.results + 1] = {
|
||||||
|
name = current_test and (current_test .. ": " .. name) or name,
|
||||||
|
ok = true,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Record a failing check within the current test
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param detail string?
|
--- @param detail string?
|
||||||
function Test.check(ok, name, detail)
|
function Suite.fail(name, detail)
|
||||||
Test.results[#Test.results + 1] = {
|
Suite.results[#Suite.results + 1] = {
|
||||||
name = current_test and (current_test .. ": " .. name) or name,
|
name = current_test and (current_test .. ": " .. name) or name,
|
||||||
ok = not not ok,
|
ok = false,
|
||||||
detail = detail,
|
detail = detail,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Shallow array equality
|
--- Check a condition
|
||||||
function Test.eq(a, b)
|
--- @param ok any Truthy when the check passed
|
||||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
--- @param name string
|
||||||
if #a ~= #b then return false end
|
--- @param detail string?
|
||||||
for i = 1, #a do
|
function Suite.check(ok, name, detail)
|
||||||
if a[i] ~= b[i] then return false end
|
if ok then
|
||||||
|
Suite.pass(name)
|
||||||
|
else
|
||||||
|
Suite.fail(name, detail)
|
||||||
end
|
end
|
||||||
return true
|
end
|
||||||
|
|
||||||
|
--- Check two values or arrays are shallowly equal
|
||||||
|
function Suite.eq(a, b, name)
|
||||||
|
local equal
|
||||||
|
if type(a) ~= "table" or type(b) ~= "table" then
|
||||||
|
equal = a == b
|
||||||
|
else
|
||||||
|
equal = #a == #b
|
||||||
|
for i = 1, #a do
|
||||||
|
if a[i] ~= b[i] then equal = false break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Suite.check(equal, name, ("%s ~= %s"):format(repr(a, 1), repr(b, 1)))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Recursive table equality, keys checked from both sides
|
--- Recursive table equality, keys checked from both sides
|
||||||
function Test.deep_eq(a, b)
|
local function deep_equal(a, b)
|
||||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
||||||
for key, value in pairs(a) do
|
for key, value in pairs(a) do
|
||||||
if not Test.deep_eq(value, b[key]) then return false end
|
if not deep_equal(value, b[key]) then return false end
|
||||||
end
|
end
|
||||||
for key in pairs(b) do
|
for key in pairs(b) do
|
||||||
if a[key] == nil then return false end
|
if a[key] == nil then return false end
|
||||||
@@ -57,38 +96,31 @@ function Framework.new()
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Names of an array of roles
|
--- Check two values are equal, comparing tables recursively
|
||||||
function Test.names(roles)
|
function Suite.deep_eq(a, b, name)
|
||||||
local rtn = {}
|
Suite.check(deep_equal(a, b), name, ("%s ~= %s"):format(repr(a, 1), repr(b, 1)))
|
||||||
for index, role in ipairs(roles) do
|
|
||||||
rtn[index] = role.name
|
|
||||||
end
|
|
||||||
return rtn
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Sorted copy of an array of strings
|
--- Sorted copy of an array of strings
|
||||||
function Test.sorted(list)
|
function Suite.sorted(list)
|
||||||
local rtn = {}
|
local rtn = {}
|
||||||
for index, value in ipairs(list) do rtn[index] = value end
|
for index, value in ipairs(list) do rtn[index] = value end
|
||||||
table.sort(rtn)
|
table.sort(rtn)
|
||||||
return rtn
|
return rtn
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Run every declared test against a fresh environment
|
--- Run every declared test, each against a fresh environment, and return
|
||||||
--- @param make_env fun(): table
|
--- the results as JSON for the javascript side
|
||||||
function Test.run(make_env)
|
function Suite.run()
|
||||||
for _, test in ipairs(Test.tests) do
|
for _, test in ipairs(Suite.tests) do
|
||||||
current_test = test.name
|
current_test = test.name
|
||||||
local ok, err = pcall(test.fn, make_env())
|
local ok, err = pcall(test.fn, make_env())
|
||||||
if not ok then
|
if not ok then
|
||||||
Test.check(false, "did not error", tostring(err))
|
Suite.fail("did not error", tostring(err))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
current_test = nil
|
current_test = nil
|
||||||
end
|
|
||||||
|
|
||||||
--- Encode the results as JSON for the javascript side
|
|
||||||
function Test.results_json()
|
|
||||||
local function escape(value)
|
local function escape(value)
|
||||||
return (value:gsub('[%c"\\]', function(c)
|
return (value:gsub('[%c"\\]', function(c)
|
||||||
return string.format("\\u%04x", c:byte())
|
return string.format("\\u%04x", c:byte())
|
||||||
@@ -96,14 +128,14 @@ function Framework.new()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for index, result in ipairs(Test.results) do
|
for index, result in ipairs(Suite.results) do
|
||||||
local detail = result.detail and string.format(',"detail":"%s"', escape(result.detail)) or ""
|
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)
|
parts[index] = string.format('{"name":"%s","ok":%s%s}', escape(result.name), tostring(result.ok), detail)
|
||||||
end
|
end
|
||||||
return "[" .. table.concat(parts, ",") .. "]"
|
return "[" .. table.concat(parts, ",") .. "]"
|
||||||
end
|
end
|
||||||
|
|
||||||
return Test
|
return Suite
|
||||||
end
|
end
|
||||||
|
|
||||||
return Framework
|
return Framework
|
||||||
|
|||||||
+68
-9
@@ -3,8 +3,8 @@ Provides the surface a clusterio module touches, recording what the module does
|
|||||||
to it. Every stub raises an error when an unimplemented property is read, which
|
to it. Every stub raises an error when an unimplemented property is read, which
|
||||||
mirrors the game api and catches mistakes in tests early.
|
mirrors the game api and catches mistakes in tests early.
|
||||||
|
|
||||||
Plugins compose these from their own test/lua/env.lua, adding stubs of their
|
Plugins compose these from their own test/lua/env.lua, adding their own stubs
|
||||||
own through `stubs.requires` and `Stubs.strict`.
|
with the extend methods rather than by mutating the tables directly.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local Stubs = {}
|
local Stubs = {}
|
||||||
@@ -29,6 +29,25 @@ function Stubs.strict(name, tbl, allowed_nil)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Merge extra into target recursively, raising when a value already exists
|
||||||
|
--- @generic T : table
|
||||||
|
--- @param target T
|
||||||
|
--- @param extra table
|
||||||
|
--- @return T
|
||||||
|
function Stubs.extend(target, extra)
|
||||||
|
for key, value in pairs(extra) do
|
||||||
|
local existing = rawget(target, key)
|
||||||
|
if existing == nil then
|
||||||
|
rawset(target, key, value)
|
||||||
|
elseif type(existing) == "table" and type(value) == "table" then
|
||||||
|
Stubs.extend(existing, value)
|
||||||
|
else
|
||||||
|
error("Stub already implements: " .. tostring(key), 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return target
|
||||||
|
end
|
||||||
|
|
||||||
--- Create an independent set of stubs, installed as the lua globals
|
--- Create an independent set of stubs, installed as the lua globals
|
||||||
function Stubs.new()
|
function Stubs.new()
|
||||||
local stubs = {
|
local stubs = {
|
||||||
@@ -39,7 +58,8 @@ function Stubs.new()
|
|||||||
}
|
}
|
||||||
|
|
||||||
local next_event_id = 100
|
local next_event_id = 100
|
||||||
script = Stubs.strict("script", {
|
local registered_metatables = {} --- @type table<table, true>
|
||||||
|
script = Stubs.strict("LuaBootstrap", {
|
||||||
generate_event_name = function()
|
generate_event_name = function()
|
||||||
next_event_id = next_event_id + 1
|
next_event_id = next_event_id + 1
|
||||||
return next_event_id
|
return next_event_id
|
||||||
@@ -47,7 +67,9 @@ function Stubs.new()
|
|||||||
raise_event = function(id, data)
|
raise_event = function(id, data)
|
||||||
stubs.events[#stubs.events + 1] = { id = id, data = data }
|
stubs.events[#stubs.events + 1] = { id = id, data = data }
|
||||||
end,
|
end,
|
||||||
register_metatable = function() end,
|
register_metatable = function(_, metatable)
|
||||||
|
registered_metatables[metatable] = true
|
||||||
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
defines = Stubs.strict("defines", {
|
defines = Stubs.strict("defines", {
|
||||||
@@ -58,7 +80,7 @@ function Stubs.new()
|
|||||||
})
|
})
|
||||||
|
|
||||||
local players_by_name, players_by_index, connected = {}, {}, {}
|
local players_by_name, players_by_index, connected = {}, {}, {}
|
||||||
game = Stubs.strict("game", {
|
game = Stubs.strict("LuaGameScript", {
|
||||||
tick = 1,
|
tick = 1,
|
||||||
players = players_by_name,
|
players = players_by_name,
|
||||||
connected_players = connected,
|
connected_players = connected,
|
||||||
@@ -87,10 +109,10 @@ function Stubs.new()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- A player object which represents the server
|
--- A player object which represents the server
|
||||||
stubs.server = Stubs.strict("server player", { index = 0, name = "<server>" })
|
stubs.server = Stubs.strict("LuaPlayer <server>", { index = 0, name = "<server>" })
|
||||||
|
|
||||||
--- Modules resolved by the stubbed require, extend before loading the module
|
--- Modules resolved by the stubbed require, add to them with extend_requires
|
||||||
stubs.requires = {
|
local requires = {
|
||||||
["modules/clusterio/api"] = Stubs.strict("clusterio api", {
|
["modules/clusterio/api"] = Stubs.strict("clusterio api", {
|
||||||
send_json = function(channel, data)
|
send_json = function(channel, data)
|
||||||
stubs.sent[#stubs.sent + 1] = { channel = channel, data = data }
|
stubs.sent[#stubs.sent + 1] = { channel = channel, data = data }
|
||||||
@@ -104,7 +126,44 @@ function Stubs.new()
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
require = function(name)
|
require = function(name)
|
||||||
return assert(stubs.requires[name], "Unexpected require: " .. name)
|
return assert(rawget(requires, name), "Unexpected require: " .. name)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Add modules or properties to the stubbed require
|
||||||
|
function stubs.extend_requires(extra) return Stubs.extend(requires, extra) end
|
||||||
|
|
||||||
|
--- Add properties to the script, game, and defines globals
|
||||||
|
function stubs.extend_script(extra) return Stubs.extend(script, extra) end
|
||||||
|
function stubs.extend_game(extra) return Stubs.extend(game, extra) end
|
||||||
|
function stubs.extend_defines(extra) return Stubs.extend(defines, extra) end
|
||||||
|
|
||||||
|
--- Copy a value the way factorio saves script data: functions are refused
|
||||||
|
--- and only metatables registered with script.register_metatable survive
|
||||||
|
local function save_load_copy(value, copies)
|
||||||
|
if type(value) == "function" then
|
||||||
|
error("Functions can not be stored in script data", 0)
|
||||||
|
end
|
||||||
|
if type(value) ~= "table" then return value end
|
||||||
|
if copies[value] then return copies[value] end
|
||||||
|
|
||||||
|
local copy = {}
|
||||||
|
copies[value] = copy
|
||||||
|
for key, entry in pairs(value) do
|
||||||
|
copy[save_load_copy(key, copies)] = save_load_copy(entry, copies)
|
||||||
|
end
|
||||||
|
|
||||||
|
local metatable = getmetatable(value)
|
||||||
|
if metatable ~= nil and registered_metatables[metatable] then
|
||||||
|
setmetatable(copy, metatable)
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Replace the script data with a copy of itself as if the map was saved
|
||||||
|
--- and loaded, the module's on_load handler should be called afterwards
|
||||||
|
function stubs.save_load()
|
||||||
|
local compat = requires["modules/clusterio/compat"]
|
||||||
|
compat.script_data = save_load_copy(compat.script_data, {})
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Forget everything recorded so far
|
--- Forget everything recorded so far
|
||||||
|
|||||||
Reference in New Issue
Block a user