factorio-test-cli 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -57
- package/cli.js +3 -0
- package/mod-setup.js +46 -21
- package/mod-setup.test.js +11 -6
- package/package.json +1 -1
- package/process-utils.js +1 -1
- package/run.js +10 -5
package/README.md
CHANGED
|
@@ -9,60 +9,3 @@ If using an npm package, you can install `factorio-test-cli` to your dev depende
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install --save-dev factorio-test-cli
|
|
11
11
|
```
|
|
12
|
-
|
|
13
|
-
## Configuration Architecture
|
|
14
|
-
|
|
15
|
-
### Config Categories
|
|
16
|
-
|
|
17
|
-
| Category | Casing | Location | Example Fields |
|
|
18
|
-
|----------|--------|----------|----------------|
|
|
19
|
-
| CLI-only | camelCase | `cli-config.ts` | `config`, `graphics`, `watch` |
|
|
20
|
-
| File+CLI | camelCase | `cli-config.ts` | `modPath`, `factorioPath`, `verbose`, `forbidOnly` |
|
|
21
|
-
| Test | snake_case | `types/config.d.ts` | `test_pattern`, `game_speed`, `bail` |
|
|
22
|
-
|
|
23
|
-
- **CLI-only**: Options only available via command line, not in config files
|
|
24
|
-
- **File+CLI**: Options that can be set in `factorio-test.json` or via command line (CLI overrides file)
|
|
25
|
-
- **Test**: Runner configuration passed to the Factorio mod; uses snake_case for Lua compatibility
|
|
26
|
-
|
|
27
|
-
### Type Hierarchy
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
types/config.d.ts
|
|
31
|
-
└── TestRunnerConfig # CLI-passable fields (source of truth)
|
|
32
|
-
│
|
|
33
|
-
├── Extended by: FactorioTest.Config (types/index.d.ts)
|
|
34
|
-
│ └── Adds mod-only fields: default_ticks_between_tests,
|
|
35
|
-
│ before_test_run, after_test_run, sound_effects
|
|
36
|
-
│
|
|
37
|
-
└── Validated by: testRunnerConfigSchema (cli/config/test-config.ts)
|
|
38
|
-
└── Zod schema for runtime validation
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Data Flow
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
CLI args ─────────────────┐
|
|
45
|
-
▼
|
|
46
|
-
factorio-test.json ──► loadConfig() ──► mergeCliConfig() ──► RunOptions
|
|
47
|
-
│
|
|
48
|
-
▼
|
|
49
|
-
buildTestConfig() ──► TestRunnerConfig ──► Factorio mod
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
1. `loadConfig()` reads `factorio-test.json` (or `package.json["factorio-test"]`)
|
|
53
|
-
2. `mergeCliConfig()` merges file config with CLI options (CLI wins)
|
|
54
|
-
3. `buildTestConfig()` extracts test runner options, combining patterns with OR logic
|
|
55
|
-
|
|
56
|
-
### File Organization
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
types/
|
|
60
|
-
├── config.d.ts # TestRunnerConfig interface (source of truth for CLI-passable test options)
|
|
61
|
-
└── index.d.ts # FactorioTest.Config extends TestRunnerConfig with mod-only fields
|
|
62
|
-
|
|
63
|
-
cli/config/
|
|
64
|
-
├── index.ts # Re-exports all public APIs
|
|
65
|
-
├── test-config.ts # Zod schema validating TestRunnerConfig + CLI registration
|
|
66
|
-
├── cli-config.ts # CliConfig + CliOnlyOptions schemas + CLI registration
|
|
67
|
-
└── loader.ts # Config loading, path resolution, merging, RunOptions type
|
|
68
|
-
```
|
package/cli.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { program } from "commander";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
4
5
|
import { CliError } from "./cli-error.js";
|
|
5
6
|
import "./run.js";
|
|
7
|
+
const { version } = JSON.parse(readFileSync(new URL("package.json", import.meta.url), "utf8"));
|
|
6
8
|
try {
|
|
7
9
|
await program
|
|
8
10
|
.name("factorio-test")
|
|
11
|
+
.version(version)
|
|
9
12
|
.description("cli for factorio testing")
|
|
10
13
|
.helpCommand(true)
|
|
11
14
|
.showHelpAfterError()
|
package/mod-setup.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runScript, runProcess } from "./process-utils.js";
|
|
|
5
5
|
import { getFactorioPlayerDataPath } from "./factorio-process.js";
|
|
6
6
|
import { CliError } from "./cli-error.js";
|
|
7
7
|
const MIN_FACTORIO_TEST_VERSION = "3.0.0";
|
|
8
|
+
const BUILTIN_MODS = new Set(["base", "quality", "elevated-rails", "space-age"]);
|
|
8
9
|
function parseVersion(version) {
|
|
9
10
|
const parts = version.split(".").map(Number);
|
|
10
11
|
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
@@ -118,12 +119,12 @@ export async function installFactorioTest(modsDir) {
|
|
|
118
119
|
const playerDataPath = getFactorioPlayerDataPath();
|
|
119
120
|
let version = await getInstalledModVersion(modsDir, "factorio-test");
|
|
120
121
|
if (!version) {
|
|
121
|
-
console.log("Downloading factorio-test
|
|
122
|
+
console.log("Downloading mod: factorio-test");
|
|
122
123
|
await runScript("fmtk", "mods", "install", "--modsPath", modsDir, "--playerData", playerDataPath, "factorio-test");
|
|
123
124
|
version = await getInstalledModVersion(modsDir, "factorio-test");
|
|
124
125
|
}
|
|
125
126
|
else if (compareVersions(version, MIN_FACTORIO_TEST_VERSION) < 0) {
|
|
126
|
-
console.log(`factorio-test ${version} is
|
|
127
|
+
console.log(`Updating mod: factorio-test (${version} is below minimum ${MIN_FACTORIO_TEST_VERSION})`);
|
|
127
128
|
await runScript("fmtk", "mods", "install", "--force", "--modsPath", modsDir, "--playerData", playerDataPath, "factorio-test");
|
|
128
129
|
version = await getInstalledModVersion(modsDir, "factorio-test");
|
|
129
130
|
}
|
|
@@ -179,22 +180,54 @@ export async function resetAutorunSettings(modsDir, verbose) {
|
|
|
179
180
|
console.log("Disabling auto-start settings");
|
|
180
181
|
await runScript("fmtk", "settings", "set", "startup", "factorio-test-auto-start-config", "{}", "--modsPath", modsDir);
|
|
181
182
|
}
|
|
183
|
+
export function parseModRequirement(spec) {
|
|
184
|
+
const trimmed = spec.trim();
|
|
185
|
+
if (trimmed.startsWith("?") || trimmed.startsWith("!") || trimmed.startsWith("(?)")) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const withoutPrefix = trimmed.startsWith("~") ? trimmed.slice(1).trim() : trimmed;
|
|
189
|
+
const match = withoutPrefix.match(/^(\S+)(?:\s*>=?\s*(\d+\.\d+\.\d+))?/);
|
|
190
|
+
if (!match)
|
|
191
|
+
return undefined;
|
|
192
|
+
const name = match[1];
|
|
193
|
+
if (!name || BUILTIN_MODS.has(name))
|
|
194
|
+
return undefined;
|
|
195
|
+
return { name, minVersion: match[2] };
|
|
196
|
+
}
|
|
182
197
|
export function parseRequiredDependencies(dependencies) {
|
|
183
198
|
const result = [];
|
|
184
199
|
for (const dep of dependencies) {
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
200
|
+
const req = parseModRequirement(dep);
|
|
201
|
+
if (req)
|
|
202
|
+
result.push(req);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
export async function installMods(modsDir, mods) {
|
|
207
|
+
const playerDataPath = getFactorioPlayerDataPath();
|
|
208
|
+
for (const { name, minVersion } of mods) {
|
|
209
|
+
const installedVersion = await getInstalledModVersion(modsDir, name);
|
|
210
|
+
if (installedVersion) {
|
|
211
|
+
if (!minVersion || compareVersions(installedVersion, minVersion) >= 0)
|
|
212
|
+
continue;
|
|
213
|
+
console.log(`Updating mod: ${name} (${installedVersion} is below required ${minVersion})`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.log(`Downloading mod: ${name}`);
|
|
188
217
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
try {
|
|
219
|
+
const args = ["fmtk", "mods", "install", "--modsPath", modsDir, "--playerData", playerDataPath];
|
|
220
|
+
if (installedVersion)
|
|
221
|
+
args.push("--force");
|
|
222
|
+
args.push(name);
|
|
223
|
+
await runScript(...args);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
console.log(`Could not download mod: ${name}`);
|
|
193
227
|
}
|
|
194
228
|
}
|
|
195
|
-
return result;
|
|
196
229
|
}
|
|
197
|
-
export async function installModDependencies(modsDir, modPath
|
|
230
|
+
export async function installModDependencies(modsDir, modPath) {
|
|
198
231
|
const infoJsonPath = path.join(modPath, "info.json");
|
|
199
232
|
let infoJson;
|
|
200
233
|
try {
|
|
@@ -207,16 +240,8 @@ export async function installModDependencies(modsDir, modPath, verbose) {
|
|
|
207
240
|
if (!Array.isArray(dependencies))
|
|
208
241
|
return [];
|
|
209
242
|
const required = parseRequiredDependencies(dependencies);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const exists = await checkModExists(modsDir, modName);
|
|
213
|
-
if (exists)
|
|
214
|
-
continue;
|
|
215
|
-
if (verbose)
|
|
216
|
-
console.log(`Installing dependency: ${modName}`);
|
|
217
|
-
await runScript("fmtk", "mods", "install", "--modsPath", modsDir, "--playerData", playerDataPath, modName);
|
|
218
|
-
}
|
|
219
|
-
return required;
|
|
243
|
+
await installMods(modsDir, required);
|
|
244
|
+
return required.map((r) => r.name);
|
|
220
245
|
}
|
|
221
246
|
export async function resolveModWatchTarget(modsDir, modPath, modName) {
|
|
222
247
|
if (modPath) {
|
package/mod-setup.test.js
CHANGED
|
@@ -3,15 +3,16 @@ import { parseRequiredDependencies } from "./mod-setup.js";
|
|
|
3
3
|
describe("parseRequiredDependencies", () => {
|
|
4
4
|
it.each([
|
|
5
5
|
[[], []],
|
|
6
|
-
[["some-mod"], ["some-mod"]],
|
|
7
|
-
[["some-mod >= 1.0.0"], ["some-mod"]],
|
|
8
|
-
[["~ some-mod >= 1.0.0"], ["some-mod"]],
|
|
6
|
+
[["some-mod"], [{ name: "some-mod" }]],
|
|
7
|
+
[["some-mod >= 1.0.0"], [{ name: "some-mod", minVersion: "1.0.0" }]],
|
|
8
|
+
[["~ some-mod >= 1.0.0"], [{ name: "some-mod", minVersion: "1.0.0" }]],
|
|
9
9
|
[["? optional-mod >= 1.0.0"], []],
|
|
10
10
|
[["! incompatible-mod"], []],
|
|
11
11
|
[["(?) hidden-optional >= 1.0.0"], []],
|
|
12
12
|
[["base >= 1.1.0"], []],
|
|
13
|
-
[["
|
|
14
|
-
[["
|
|
13
|
+
[["quality >= 1.0.0"], []],
|
|
14
|
+
[[" mod-name >= 1.0.0 "], [{ name: "mod-name", minVersion: "1.0.0" }]],
|
|
15
|
+
[["~ soft-mod"], [{ name: "soft-mod" }]],
|
|
15
16
|
[
|
|
16
17
|
[
|
|
17
18
|
"base >= 1.1.0",
|
|
@@ -22,7 +23,11 @@ describe("parseRequiredDependencies", () => {
|
|
|
22
23
|
"~ soft-required >= 1.0.0",
|
|
23
24
|
"another-required",
|
|
24
25
|
],
|
|
25
|
-
[
|
|
26
|
+
[
|
|
27
|
+
{ name: "required-mod", minVersion: "2.0.0" },
|
|
28
|
+
{ name: "soft-required", minVersion: "1.0.0" },
|
|
29
|
+
{ name: "another-required" },
|
|
30
|
+
],
|
|
26
31
|
],
|
|
27
32
|
])("parseRequiredDependencies(%j) => %j", (input, expected) => {
|
|
28
33
|
expect(parseRequiredDependencies(input)).toEqual(expected);
|
package/package.json
CHANGED
package/process-utils.js
CHANGED
|
@@ -5,7 +5,7 @@ export function setVerbose(v) {
|
|
|
5
5
|
verbose = v;
|
|
6
6
|
}
|
|
7
7
|
export function runScript(...command) {
|
|
8
|
-
return runProcess(
|
|
8
|
+
return runProcess(verbose, "npx", ...command);
|
|
9
9
|
}
|
|
10
10
|
export function runProcess(inheritStdio, command, ...args) {
|
|
11
11
|
if (verbose)
|
package/run.js
CHANGED
|
@@ -8,7 +8,7 @@ import { buildTestConfig, loadConfig, mergeCliConfig, registerAllCliOptions, } f
|
|
|
8
8
|
import { autoDetectFactorioPath } from "./factorio-process.js";
|
|
9
9
|
import { getHeadlessSavePath, runFactorioTestsGraphics, runFactorioTestsHeadless, } from "./factorio-process.js";
|
|
10
10
|
import { watchDirectory, watchFile } from "./file-watcher.js";
|
|
11
|
-
import { configureModToTest, ensureConfigIni, installFactorioTest, installModDependencies, resetAutorunSettings, resolveModWatchTarget, setSettingsForAutorun, } from "./mod-setup.js";
|
|
11
|
+
import { configureModToTest, ensureConfigIni, installFactorioTest, installModDependencies, installMods, parseModRequirement, resetAutorunSettings, resolveModWatchTarget, setSettingsForAutorun, } from "./mod-setup.js";
|
|
12
12
|
import { runScript, setVerbose } from "./process-utils.js";
|
|
13
13
|
import { getDefaultOutputPath, readPreviousFailedTests, writeResultsFile } from "./test-results.js";
|
|
14
14
|
const thisCommand = program
|
|
@@ -48,15 +48,20 @@ async function setupTestRun(patterns, options) {
|
|
|
48
48
|
const modsDir = path.join(dataDir, "mods");
|
|
49
49
|
await fsp.mkdir(modsDir, { recursive: true });
|
|
50
50
|
const modToTest = await configureModToTest(modsDir, options.modPath, options.modName, options.verbose);
|
|
51
|
-
const modDependencies = options.modPath
|
|
52
|
-
? await installModDependencies(modsDir, path.resolve(options.modPath), options.verbose)
|
|
53
|
-
: [];
|
|
51
|
+
const modDependencies = options.modPath ? await installModDependencies(modsDir, path.resolve(options.modPath)) : [];
|
|
54
52
|
await installFactorioTest(modsDir);
|
|
53
|
+
const configModRequirements = options.mods
|
|
54
|
+
?.filter((m) => !m.match(/^\S+=(?:true|false)$/))
|
|
55
|
+
.map(parseModRequirement)
|
|
56
|
+
.filter((r) => r != null) ?? [];
|
|
57
|
+
if (configModRequirements.length > 0) {
|
|
58
|
+
await installMods(modsDir, configModRequirements);
|
|
59
|
+
}
|
|
55
60
|
const enableModsOptions = [
|
|
56
61
|
"factorio-test=true",
|
|
57
62
|
`${modToTest}=true`,
|
|
58
63
|
...modDependencies.map((m) => `${m}=true`),
|
|
59
|
-
...(options.mods?.map((m) => (m.
|
|
64
|
+
...(options.mods?.map((m) => (m.match(/^\S+=(?:true|false)$/) ? m : `${m.split(/\s/)[0]}=true`)) ?? []),
|
|
60
65
|
];
|
|
61
66
|
if (options.verbose)
|
|
62
67
|
console.log("Adjusting mods");
|