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 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 from mod portal using fmtk.");
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 outdated, downloading latest version.`);
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 trimmed = dep.trim();
186
- if (trimmed.startsWith("?") || trimmed.startsWith("!") || trimmed.startsWith("(?)")) {
187
- continue;
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
- const withoutPrefix = trimmed.startsWith("~") ? trimmed.slice(1).trim() : trimmed;
190
- const modName = withoutPrefix.split(/\s/)[0];
191
- if (modName && modName !== "base") {
192
- result.push(modName);
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, verbose) {
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
- const playerDataPath = getFactorioPlayerDataPath();
211
- for (const modName of required) {
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
- [[" mod-name >= 1.0.0 "], ["mod-name"]],
14
- [["~ soft-mod"], ["soft-mod"]],
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
- ["required-mod", "soft-required", "another-required"],
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "factorio-test-cli",
3
- "version": "3.0.5",
3
+ "version": "3.1.1",
4
4
  "description": "A CLI to run FactorioTest.",
5
5
  "license": "MIT",
6
6
  "repository": {
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(true, "npx", ...command);
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.includes("=") ? m : `${m}=true`)) ?? []),
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");