Address review on the test harness

- The generic parts move to test/ in the repository root so other plugins can
  reuse them: the factorio and clusterio stubs, the test framework, the
  fengari runner, and clusterio's testMatrix and round trip helpers. A plugin
  composes them from its own env.lua, which adds its stubs and fixtures.
- Tests are declared with Test.test(name, fn) and every test function
  receives a fresh environment, so nothing carries over between them. A test
  which errors is reported as a failure rather than aborting the file.
- Every stub raises on properties it does not implement, which mirrors the
  game api. game.player is the one property allowed to read as nil.
- Test.deep_eq compares tables recursively with keys checked from both sides.
- The message records round trip through the same testMatrix and
  testRoundTripJsonSerialisable helpers the clusterio tests use, covering
  every optional field combination of the records, events and requests.
- controller.test.js and instance.test.js cover the node side of the plugin
  against faked controller and instance internals: property creation and
  sweeping, record building, broadcasts, subscription replay, assignment
  validation, auto assignment and its blocking role, seeding, the initialise
  payload, sync mode gating, and the rejection rollback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bbassie
2026-08-22 16:45:51 +00:00
co-authored by Claude Fable 5
parent 59a0e0be13
commit 24d36db601
14 changed files with 1106 additions and 434 deletions
+63
View File
@@ -0,0 +1,63 @@
"use strict";
const path = require("node:path");
const fs = require("node:fs");
const { lua, lauxlib, lualib, to_luastring, to_jsstring } = require("fengari");
/**
* Run one lua test file in its own lua state and return its results.
*
* The state is created here, on first use by the caller, so every test file
* gets an independent set of stubs. Three chunks run in order, each taking one
* argument and returning one value:
*
* 1. The plugin's env.lua, receiving this directory so it can load the shared
* stubs and framework, returning its environment module.
* 2. The test file, receiving the environment module, declaring its tests.
* 3. The tests then run, each against a fresh environment, and the results
* are returned as JSON.
*
* @param {string} envFile - Absolute path of the plugin's test/lua/env.lua.
* @param {string} testFile - Absolute path of the lua test file to run.
* @returns {{ name: string, ok: boolean, detail?: string }[]}
*/
function runLuaTests(envFile, testFile) {
const L = lauxlib.luaL_newstate();
lualib.luaL_openlibs(L);
const load = (file) => {
const code = fs.readFileSync(file);
const status = lauxlib.luaL_loadbuffer(L, code, code.length, to_luastring(`@${file}`));
if (status !== lua.LUA_OK) {
throw new Error(to_jsstring(lua.lua_tostring(L, -1)));
}
};
const call = () => {
if (lua.lua_pcall(L, 1, 1, 0) !== lua.LUA_OK) {
throw new Error(to_jsstring(lua.lua_tostring(L, -1)));
}
};
load(envFile);
lua.lua_pushstring(L, to_luastring(__dirname));
call();
// The environment stays on the stack and is passed to the test chunk
load(testFile);
lua.lua_insert(L, -2);
call();
const json = to_jsstring(lua.lua_tostring(L, -1));
return JSON.parse(json);
}
/** Report the results of a lua test file through a tap test object. */
function reportLuaTests(t, envFile, testFile) {
const results = runLuaTests(envFile, testFile);
t.ok(results.length > 0, `${path.basename(testFile)} produced results`);
for (const result of results) {
t.ok(result.ok, result.name, result.detail ? { detail: result.detail } : {});
}
t.end();
}
module.exports = { runLuaTests, reportLuaTests };