diff --git a/exp_roles/package.json b/exp_roles/package.json index 1e462135..05f53e71 100644 --- a/exp_roles/package.json +++ b/exp_roles/package.json @@ -8,7 +8,8 @@ "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --env production", - "test": "tap --disable-coverage --allow-empty-coverage test/*.test.js" + "test": "tap --disable-coverage --allow-empty-coverage test/*.test.js", + "coverage": "tap --coverage-report=text test/*.test.js" }, "engines": { "node": ">=18" diff --git a/exp_roles/test/controller.test.js b/exp_roles/test/controller.test.js index 8b47e5cb..bf2f843c 100644 --- a/exp_roles/test/controller.test.js +++ b/exp_roles/test/controller.test.js @@ -20,6 +20,12 @@ lib.Link.register(messages.AssignmentUpdateRequest); const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} }; +// Silence the singleton logger to avoid polluting the test output +lib.logger.silent = true; +t.after(() => { + lib.logger.silent = false; +}); + /** Build a plugin around a real controller, which is side effect free while not started. */ async function startPlugin(t2, { roles = [] } = {}) { const controllerConfig = new lib.ControllerConfig("controller", { @@ -173,6 +179,18 @@ t.test("class ControllerPlugin", t2 => { t3.strictSame(applied.length, 1, "other player events do not"); }); + t2.test(".handleRoleListRequest() returns the current roles", async t3 => { + const { plugin } = await startPlugin(t3, { + roles: [role(1, "Player"), role(5, "Moderator")], + }); + plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 0, "Mod")); + + const response = await plugin.handleRoleListRequest(new messages.RoleListRequest()); + t3.strictSame(response.length, 2, "the current roles are returned"); + t3.ok(response.some(record => record.name === "Moderator")); + t3.ok(response.some(record => record.name === "Player")); + }); + t2.test(".handleRoleMetaUpdateRequest() validates the role", async t3 => { const { plugin } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] }); @@ -187,6 +205,19 @@ t.test("class ControllerPlugin", t2 => { t3.strictSame(plugin.roleMeta.get(5).shortHand, "Mod", "the properties are stored"); }); + t2.test(".handleAssignmentListRequest() returns the current assignments", async t3 => { + const { plugin, controller } = await startPlugin(t3, { + roles: [role(1, "Player"), role(5, "Moderator")], + }); + controller.users.createUser("alice").set("roleIds", new Set([5])); + controller.users.createUser("bob"); + + const response = await plugin.handleAssignmentListRequest(new messages.AssignmentListRequest()); + t3.strictSame(response.length, 1, "the current assignments are returned"); + t3.strictSame(response[0].name, "alice"); + t3.strictSame(response[0].roleIds, new Set([5]), "the default role is not listed"); + }); + t2.test(".handleAssignmentSubscription() replays only newer records", async t3 => { const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] }); controller.users.createUser("alice").set("roleIds", new Set([5])); diff --git a/exp_roles/test/messages.test.js b/exp_roles/test/messages.test.js index ba89b550..8ec26550 100644 --- a/exp_roles/test/messages.test.js +++ b/exp_roles/test/messages.test.js @@ -7,18 +7,18 @@ const fullMeta = new messages.RoleMetaRecord( 7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false, ); -t.test("class RoleColor", subtest => { - testRoundTripJsonSerialisable(messages.RoleColor, testMatrix( +t.test("class RoleColor", t2 => { + testRoundTripJsonSerialisable(t2, messages.RoleColor, testMatrix( [0, 255], // r [0, 128], // g [0, 1], // b )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("class RoleMetaRecord", subtest => { - testRoundTripJsonSerialisable(messages.RoleMetaRecord, testMatrix( +t.test("class RoleMetaRecord", t2 => { + testRoundTripJsonSerialisable(t2, messages.RoleMetaRecord, testMatrix( [7], // id [3], // order [0, 1], // priority @@ -30,12 +30,12 @@ t.test("class RoleMetaRecord", subtest => { [0, 12345], // updatedAtMs [false, true], // isDeleted )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("class RoleRecord", subtest => { - testRoundTripJsonSerialisable(messages.RoleRecord, testMatrix( +t.test("class RoleRecord", t2 => { + testRoundTripJsonSerialisable(t2, messages.RoleRecord, testMatrix( [7], // id ["Moderator"], // name [[], ["a.b", "c.d"]], // permissions @@ -44,67 +44,73 @@ t.test("class RoleRecord", subtest => { [0, 12345], // updatedAtMs [false, true], // isDeleted )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("class AssignmentRecord", subtest => { - testRoundTripJsonSerialisable(messages.AssignmentRecord, testMatrix( +t.test("class AssignmentRecord", t2 => { + testRoundTripJsonSerialisable(t2, messages.AssignmentRecord, testMatrix( ["alice"], // name [new Set(), new Set([5, 6])], // roleIds [0, 12345], // updatedAtMs [false, true], // isDeleted )); - subtest.pass("round trips"); - subtest.end(); + + t2.test("get id()", t3 => { + const record = new messages.AssignmentRecord("alice", new Set([5]), 12345); + t3.equal(record.id, "alice"); + t3.end(); + }); + + t2.end(); }); const sampleRecord = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345); const sampleAssignment = new messages.AssignmentRecord("alice", new Set([5]), 12345); -t.test("class RoleUpdatedEvent", subtest => { - testRoundTripJsonSerialisable(messages.RoleUpdatedEvent, testMatrix( +t.test("class RoleUpdatedEvent", t2 => { + testRoundTripJsonSerialisable(t2, messages.RoleUpdatedEvent, testMatrix( [[], [sampleRecord]], // updates )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("class AssignmentUpdatedEvent", subtest => { - testRoundTripJsonSerialisable(messages.AssignmentUpdatedEvent, testMatrix( +t.test("class AssignmentUpdatedEvent", t2 => { + testRoundTripJsonSerialisable(t2, messages.AssignmentUpdatedEvent, testMatrix( [[], [sampleAssignment]], // updates )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("class RoleMetaUpdateRequest", subtest => { - testRoundTripJsonSerialisable(messages.RoleMetaUpdateRequest, testMatrix( +t.test("class RoleMetaUpdateRequest", t2 => { + testRoundTripJsonSerialisable(t2, messages.RoleMetaUpdateRequest, testMatrix( [new messages.RoleMetaRecord(7, 3), fullMeta], // meta )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("class AssignmentUpdateRequest", subtest => { - testRoundTripJsonSerialisable(messages.AssignmentUpdateRequest, testMatrix( +t.test("class AssignmentUpdateRequest", t2 => { + testRoundTripJsonSerialisable(t2, messages.AssignmentUpdateRequest, testMatrix( ["alice"], // name [[], [5]], // assign [[], [6]], // unassign )); - subtest.pass("round trips"); - subtest.end(); + + t2.end(); }); -t.test("encodeRolesForLua() sends each permission name once", subtest => { +t.test("encodeRolesForLua() sends each permission name once", t2 => { const meta = id => new messages.RoleMetaRecord(id, id); const encoded = messages.encodeRolesForLua([ new messages.RoleRecord(1, "A", ["p.one", "p.two"], meta(1)), new messages.RoleRecord(2, "B", ["p.two", "p.three"], meta(2)), ]); - subtest.strictSame(encoded.permission_names, ["p.one", "p.two", "p.three"], "names are deduplicated"); - subtest.strictSame(encoded.roles[0].permissions, [0, 1], "indexes are zero based"); - subtest.strictSame(encoded.roles[1].permissions, [1, 2], "shared names reuse their index"); - subtest.end(); + t2.strictSame(encoded.permission_names, ["p.one", "p.two", "p.three"], "names are deduplicated"); + t2.strictSame(encoded.roles[0].permissions, [0, 1], "indexes are zero based"); + t2.strictSame(encoded.roles[1].permissions, [1, 2], "shared names reuse their index"); + t2.end(); }); diff --git a/exp_roles/test/control.test.js b/exp_roles/test/module.test.js similarity index 62% rename from exp_roles/test/control.test.js rename to exp_roles/test/module.test.js index 70272979..2e5d4aa5 100644 --- a/exp_roles/test/control.test.js +++ b/exp_roles/test/module.test.js @@ -3,12 +3,12 @@ const path = require("node:path"); const t = require("tap"); const { reportLuaTests } = require("../../test/lua/runner"); -const envFile = path.join(__dirname, "lua", "env.lua"); +const envFile = path.join(__dirname, "module", "env.lua"); // Each file runs in its own lua state, and each test in a fresh environment -t.test("module/control.lua", t2 => { +t.test("control.lua", t2 => { 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.test(file, t3 => reportLuaTests(t3, envFile, path.join(__dirname, "module", file))); } t2.end(); }); diff --git a/exp_roles/test/lua/assignment.lua b/exp_roles/test/module/assignment.lua similarity index 98% rename from exp_roles/test/lua/assignment.lua rename to exp_roles/test/module/assignment.lua index 9a99dd58..a774af5f 100644 --- a/exp_roles/test/lua/assignment.lua +++ b/exp_roles/test/module/assignment.lua @@ -1,5 +1,5 @@ --- Tests for the assignment methods of module/control.lua -local 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 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 diff --git a/exp_roles/test/lua/env.lua b/exp_roles/test/module/env.lua similarity index 94% rename from exp_roles/test/lua/env.lua rename to exp_roles/test/module/env.lua index b1c7f908..5b612329 100644 --- a/exp_roles/test/lua/env.lua +++ b/exp_roles/test/module/env.lua @@ -8,10 +8,10 @@ returned here becomes `...` in each test file. ]] local shared_root = ... --- @type string -local source = assert(debug.getinfo(1, "S")).source -local plugin_root = assert(source:match("^@(.*)/test/lua/env%.lua$")) +local source = assert(debug.getinfo(1, "S")).source:gsub("\\", "/") +local plugin_root = assert(source:match("^@(.*)/test/module/env%.lua$")) -local Framework = assert(loadfile(shared_root .. "/framework.lua"))() +local Framework = assert(loadfile(shared_root .. "/framework.lua"))() --- @type Framework --- Standard fixture: permission names sent once and referenced by zero based index local permission_names = { @@ -48,7 +48,7 @@ for _, record in ipairs(role_records()) do end return Framework.suite(function(env) - env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))() + env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))() --- @type ExpRoles env.Roles.on_server_startup() --- Get a fixture role by name, failing the test when it does not exist diff --git a/exp_roles/test/lua/holders.lua b/exp_roles/test/module/holders.lua similarity index 96% rename from exp_roles/test/lua/holders.lua rename to exp_roles/test/module/holders.lua index 8ee4c91f..6d5cdb16 100644 --- a/exp_roles/test/lua/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 = ... --- 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 diff --git a/exp_roles/test/lua/lookup.lua b/exp_roles/test/module/lookup.lua similarity index 97% rename from exp_roles/test/lua/lookup.lua rename to exp_roles/test/module/lookup.lua index 2479e6c8..e203fbc6 100644 --- a/exp_roles/test/lua/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 = ... --- 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/lua/players.lua b/exp_roles/test/module/players.lua similarity index 97% rename from exp_roles/test/lua/players.lua rename to exp_roles/test/module/players.lua index a13dce6c..50513a30 100644 --- a/exp_roles/test/lua/players.lua +++ b/exp_roles/test/module/players.lua @@ -1,5 +1,5 @@ --- Tests for the player checks of module/control.lua -local 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/lua/sync.lua b/exp_roles/test/module/sync.lua similarity index 98% rename from exp_roles/test/lua/sync.lua rename to exp_roles/test/module/sync.lua index 2a94875d..64daf581 100644 --- a/exp_roles/test/lua/sync.lua +++ b/exp_roles/test/module/sync.lua @@ -1,5 +1,5 @@ --- Tests for the sync entry points of module/control.lua -local 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 --- alice is a moderator and carol has only the default role, both connected diff --git a/exp_roles/test/seed.test.js b/exp_roles/test/seed.test.js index 010dbd5d..adcb0987 100644 --- a/exp_roles/test/seed.test.js +++ b/exp_roles/test/seed.test.js @@ -6,35 +6,57 @@ const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed"); // Importing this defines the exp_scenario permissions the seed grants require("@expcluster/scenario/dist/node/permissions"); -t.test("seedRoles grant only defined permissions", subtest => { +t.test("seedRoles[] grant only defined permissions", t2 => { for (const role of seedRoles) { - for (const permission of flattenSeedPermissions(role)) { - subtest.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`); + for (const permission of role.permissions) { + t2.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`); } } - subtest.end(); + t2.end(); }); -t.test("flattenSeedPermissions() carries parents into their children", subtest => { +t.test("seedRoles[] are unique with one default and one admin", t2 => { + const names = seedRoles.map(role => role.name); + t2.strictSame(names.length, new Set(names).size, "names are unique"); + t2.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role"); + t2.strictSame(seedRoles.filter(role => role.isAdmin).length, 1, "one admin role"); + t2.end(); +}); + +t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => { const byName = new Map(seedRoles.map(role => [role.name, role])); for (const role of seedRoles) { if (role.parent === undefined) { continue; } + const parent = byName.get(role.parent); - subtest.ok(parent, `${role.name} has parent ${role.parent}`); + t2.ok(parent, `${role.name} has parent ${role.parent}`); + const flattened = flattenSeedPermissions(role); for (const permission of flattenSeedPermissions(parent)) { - subtest.ok(flattened.has(permission), `${role.name} inherits ${permission}`); + t2.ok(flattened.has(permission), `${role.name} inherits ${permission}`); } } - subtest.end(); + t2.end(); }); -t.test("seedRoles are unique with one default and one admin", subtest => { - const names = seedRoles.map(role => role.name); - 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.isAdmin).length, 1, "one admin role"); - subtest.end(); -}); +t.test("flattenSeedPermissions() does not inherit permissions when parent is undefined", t2 => { + for (const role of seedRoles) { + if (role.parent !== undefined) { + continue; + } + + const flattened = flattenSeedPermissions(role); + for (const permission of role.permissions) { + t2.ok(flattened.has(permission), `${role.name} contains ${permission}`); + } + + t2.equal( + flattened.size, + role.permissions.length, + `${role.name} has no inherited permissions`, + ); + } + t2.end(); +}); \ No newline at end of file diff --git a/exp_scenario/module/control/nuke_protection.lua b/exp_scenario/module/control/nuke_protection.lua index c8798388..ba8f4320 100644 --- a/exp_scenario/module/control/nuke_protection.lua +++ b/exp_scenario/module/control/nuke_protection.lua @@ -19,7 +19,7 @@ local function check_items(player, type, banned_items) local inventory = assert(player.get_inventory(type)) -- Check what items the player has for i = 1, #inventory do - local item = inventory[i] + local item = inventory[i --[[@as uint]]] if item.valid_for_read and banned_items[item.name] then player.print{ "exp_nuke-protection.chat-found", item.prototype.localised_name } items[#items + 1] = item diff --git a/exp_scenario/module/control/spawn_area.lua b/exp_scenario/module/control/spawn_area.lua index aab08d17..d4c23275 100644 --- a/exp_scenario/module/control/spawn_area.lua +++ b/exp_scenario/module/control/spawn_area.lua @@ -5,8 +5,8 @@ Adds a custom spawn area with chests and afk turrets local config = require("modules.exp_legacy.config.spawn_area") --- Apply an offset to a LuaPosition ---- @param position MapPosition ---- @param offset MapPosition +--- @param position MapPosition.struct +--- @param offset MapPosition.struct --- @return MapPosition.struct local function apply_offset(position, offset) return { @@ -17,7 +17,7 @@ end --- Apply offset to an array of positions --- @param positions table ---- @param offset MapPosition +--- @param offset MapPosition.struct --- @param x_index number --- @param y_index number local function apply_offset_to_array(positions, offset, x_index, y_index) @@ -97,14 +97,19 @@ local belt_details = { --- Makes a 2x2 afk belt at the locations in the config --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function create_belts(surface, offset) local belt_type = config.afk_belts.belt_type + local entity_pos = { x = 0, y = 0 } + local group_offset = { x = 0, y = 0 } for _, position in pairs(config.afk_belts.locations) do - position = apply_offset(position, offset) + group_offset.x = position[1] + offset.x + group_offset.y = position[2] + offset.y for _, belt in pairs(belt_details) do - local pos = apply_offset(position, belt) + entity_pos.x = belt[1] + entity_pos.y = belt[2] + local pos = apply_offset(entity_pos, group_offset) local entity = surface.create_entity{ name = belt_type, position = pos, force = "neutral", direction = belt[3] } if entity and config.afk_belts.protected then protect_entity(entity) @@ -115,13 +120,16 @@ end -- Generates extra tiles in a set pattern as defined in the config --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function create_pattern_tiles(surface, offset) local tiles_to_make = {} local pattern_tile = config.pattern.pattern_tile + local tile_pos = { x = 0, y = 0 } for index, position in pairs(config.pattern.locations) do - tiles_to_make[index] = { name = pattern_tile, position = apply_offset(position, offset) } + tile_pos.x = position[1] + tile_pos.y = position[2] + tiles_to_make[index] = { name = pattern_tile, position = apply_offset(tile_pos, offset) } end surface.set_tiles(tiles_to_make) @@ -129,13 +137,16 @@ end -- Generates extra water as defined in the config --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function create_water_tiles(surface, offset) local tiles_to_make = {} local water_tile = config.water.water_tile + local tile_pos = { x = 0, y = 0 } for _, position in pairs(config.water.locations) do - table.insert(tiles_to_make, { name = water_tile, position = apply_offset(position, offset) }) + tile_pos.x = position[1] + tile_pos.y = position[2] + table.insert(tiles_to_make, { name = water_tile, position = apply_offset(tile_pos, offset) }) end surface.set_tiles(tiles_to_make) @@ -143,11 +154,17 @@ end --- Generates the entities that are in the config --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function create_entities(surface, offset) + local entity_pos = { x = 0, y = 0 } for _, entity_details in pairs(config.entities.locations) do - local pos = apply_offset({ entity_details[2], entity_details[3] }, offset) - local entity = surface.create_entity{ name = entity_details[1], position = pos, force = "neutral" } + entity_pos.x = entity_details[2] + entity_pos.y = entity_details[3] + local entity = surface.create_entity{ + name = entity_details[1], + position = apply_offset(entity_pos, offset), + force = "neutral" + } if entity then if config.entities.protected then @@ -161,7 +178,7 @@ end --- Generates an area with no water or entities, no water area is larger --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function clear_spawn_area(surface, offset) local get_tile = surface.get_tile @@ -182,7 +199,7 @@ local function clear_spawn_area(surface, offset) for y = -fill_radius, fill_radius do -- loop over y local y_sqr = (y + 0.5) ^ 2 local dst = x_sqr + y_sqr - local pos = apply_offset({ x, y }, offset) + local pos = apply_offset({ x = x, y = y }, offset) if dst < tile_radius_sqr then -- If it is inside the decon radius always set the tile tiles_to_make[#tiles_to_make + 1] = { name = decon_tile, position = pos } @@ -204,14 +221,14 @@ end --- Spawn the resource tiles --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function create_resources_tiles(surface, offset) for _, resource in ipairs(config.resource_tiles.resources) do if resource.enabled then local pos = apply_offset(resource.offset, offset) for x = pos.x, pos.x + resource.size[1] do for y = pos.y, pos.y + resource.size[2] do - surface.create_entity{ name = resource.name, amount = resource.amount, position = { x, y } } + surface.create_entity{ name = resource.name, amount = resource.amount, position = { x = x, y = y } } end end end @@ -220,7 +237,7 @@ end --- Spawn the resource entities --- @param surface LuaSurface ---- @param offset MapPosition +--- @param offset MapPosition.struct local function create_resource_patches(surface, offset) for _, resource in ipairs(config.resource_patches.resources) do if resource.enabled then diff --git a/exp_scenario/module/gui/module_inserter.lua b/exp_scenario/module/gui/module_inserter.lua index ed320137..f930bd34 100644 --- a/exp_scenario/module/gui/module_inserter.lua +++ b/exp_scenario/module/gui/module_inserter.lua @@ -459,7 +459,7 @@ local function on_entity_settings_pasted(event) -- Get the modules and add them to the planner local all_modules = {} for i = 1, #module_inventory do - local slot = module_inventory[i] + local slot = module_inventory[i --[[@as uint]]] if slot.valid_for_read and slot.count > 0 then all_modules[i] = { name = slot.name, quality = slot.quality.name } else diff --git a/exp_scenario/module/gui/research_milestones.lua b/exp_scenario/module/gui/research_milestones.lua index 390867c8..a7e06846 100644 --- a/exp_scenario/module/gui/research_milestones.lua +++ b/exp_scenario/module/gui/research_milestones.lua @@ -294,9 +294,9 @@ end --- Get the achieved time for a force --- @param force LuaForce --- @param research_index number ---- @return number +--- @return number? function Elements.container.get_achieved_time(force, research_index) - return assert(Elements.container.data[force][research_index]) + return assert(Elements.container.data[force])[research_index] end --- Calculate the starting research index for a force diff --git a/test/common.js b/test/common.js index eb75a3f2..bf63b030 100644 --- a/test/common.js +++ b/test/common.js @@ -1,5 +1,4 @@ "use strict"; -const assert = require("assert").strict; const { compile } = require("@clusterio/lib"); /** @@ -15,22 +14,37 @@ function testMatrix(...arrays) { } /** - * Test that a class is round trip json serialisable across multiple test cases. + * Test that a class is round trip JSON serialisable across multiple test cases. * * @template {any[]} T - Constructor arguments + * @param {import("tap").Test} t - Parent test. * @param {{new(...args: T): object}} Class - The class which has toJSON and fromJSON methods. - * @param {T[]} tests - The tests inputs to pass to the class constructor. + * @param {T[]} tests - The test inputs to pass to the class constructor. */ -function testRoundTripJsonSerialisable(Class, tests) { +function testRoundTripJsonSerialisable(t, Class, tests) { const validate = compile(Class.jsonSchema); + for (const test of tests) { - const original = new Class(...test); - const serialised = JSON.stringify(original); - const jsonObject = JSON.parse(serialised); - const reconstructed = Class.fromJSON(jsonObject); - assert.deepEqual(reconstructed, original, JSON.stringify(test)); - if (!validate(jsonObject)) { - throw validate.errors; + const name = JSON.stringify(test); + + try { + const original = new Class(...test); + const jsonObject = JSON.parse(JSON.stringify(original)); + const reconstructed = Class.fromJSON(jsonObject); + + if (!validate(jsonObject)) { + t.fail(`schema validation: ${name}`, { + diagnostic: { + json: jsonObject, + errors: validate.errors, + }, + }); + continue; + } + + t.strictSame(reconstructed, original, `round trip: ${name}`); + } catch (error) { + t.fail(`round trip: ${name}`, { error }); } } } diff --git a/test/lua/framework.lua b/test/lua/framework.lua index eadca54c..9fc97207 100644 --- a/test/lua/framework.lua +++ b/test/lua/framework.lua @@ -10,6 +10,7 @@ local source = assert(debug.getinfo(1, "S")).source local shared_root = assert(source:match("^@(.*)/framework%.lua$")) local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))() +--- @class Framework local Framework = {} --- Turn a value into a string for failure messages diff --git a/test/lua/stubs.lua b/test/lua/stubs.lua index e587728e..3cc1b3a8 100644 --- a/test/lua/stubs.lua +++ b/test/lua/stubs.lua @@ -50,6 +50,7 @@ end --- Create an independent set of stubs, installed as the lua globals function Stubs.new() + --- @class Stubs local stubs = { events = {}, -- events raised through script.raise_event printed = {}, -- game.print and player.print messages