factorio-test-cli 3.0.0 → 3.0.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/config/cli-config.js +142 -0
- package/config/index.js +3 -0
- package/config/loader.js +72 -0
- package/config/test-config.js +104 -0
- package/package.json +2 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { testRunnerConfigSchema, registerTestConfigOptions } from "./test-config.js";
|
|
3
|
+
export const DEFAULT_DATA_DIRECTORY = "./factorio-test-data-dir";
|
|
4
|
+
const cliConfigFields = {
|
|
5
|
+
modPath: {
|
|
6
|
+
schema: z.string().optional(),
|
|
7
|
+
cli: {
|
|
8
|
+
flags: "-p --mod-path <path>",
|
|
9
|
+
description: "[one required] Path to the mod folder (containing info.json). Will create a symlink from mods folder to here.",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
modName: {
|
|
13
|
+
schema: z.string().optional(),
|
|
14
|
+
cli: {
|
|
15
|
+
flags: "--mod-name <name>",
|
|
16
|
+
description: "[one required] Name of a mod already in the configured data directory.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
factorioPath: {
|
|
20
|
+
schema: z.string().optional(),
|
|
21
|
+
cli: {
|
|
22
|
+
flags: "--factorio-path <path>",
|
|
23
|
+
description: "Path to the Factorio binary. If not specified, will attempt to be auto-detected.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
dataDirectory: {
|
|
27
|
+
schema: z.string().optional(),
|
|
28
|
+
cli: {
|
|
29
|
+
flags: "-d --data-directory <path>",
|
|
30
|
+
description: "Factorio data directory, where mods, saves, config etc. will be.",
|
|
31
|
+
default: DEFAULT_DATA_DIRECTORY,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
save: {
|
|
35
|
+
schema: z.string().optional(),
|
|
36
|
+
cli: {
|
|
37
|
+
flags: "--save <path>",
|
|
38
|
+
description: "Path to save file. Default: uses a bundled save with empty lab-tile world.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
mods: {
|
|
42
|
+
schema: z.array(z.string()).optional(),
|
|
43
|
+
cli: {
|
|
44
|
+
flags: "--mods <mods...>",
|
|
45
|
+
description: "Additional mods to enable besides the mod under test (e.g., --mods mod1 mod2=1.2.3).",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
factorioArgs: {
|
|
49
|
+
schema: z.array(z.string()).optional(),
|
|
50
|
+
cli: {
|
|
51
|
+
flags: "--factorio-args <args...>",
|
|
52
|
+
description: "Additional arguments to pass to Factorio process.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
verbose: {
|
|
56
|
+
schema: z.boolean().optional(),
|
|
57
|
+
cli: {
|
|
58
|
+
flags: "-v --verbose",
|
|
59
|
+
description: "Enable verbose logging; pipe Factorio output to stdout.",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
quiet: {
|
|
63
|
+
schema: z.boolean().optional(),
|
|
64
|
+
cli: {
|
|
65
|
+
flags: "-q --quiet",
|
|
66
|
+
description: "Suppress per-test output, show only final result.",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
outputFile: {
|
|
70
|
+
schema: z.string().optional(),
|
|
71
|
+
cli: {
|
|
72
|
+
flags: "--output-file <path>",
|
|
73
|
+
description: "Path for test results JSON file. Used to reorder failed tests first on subsequent runs.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
forbidOnly: {
|
|
77
|
+
schema: z.boolean().optional(),
|
|
78
|
+
cli: {
|
|
79
|
+
flags: "--forbid-only",
|
|
80
|
+
description: "Fail if .only tests are present (default: enabled). Useful for CI.",
|
|
81
|
+
negatable: true,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
watchPatterns: {
|
|
85
|
+
schema: z.array(z.string()).optional(),
|
|
86
|
+
cli: {
|
|
87
|
+
flags: "--watch-patterns <patterns...>",
|
|
88
|
+
description: "Glob patterns to watch (default: info.json, **/*.lua).",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
udpPort: {
|
|
92
|
+
schema: z.number().int().positive().optional(),
|
|
93
|
+
cli: {
|
|
94
|
+
flags: "--udp-port <port>",
|
|
95
|
+
description: "UDP port to use for --graphics --watch mode reload trigger (default: 14434).",
|
|
96
|
+
parseArg: (v) => parseInt(v, 10),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
export const cliConfigSchema = z.object({
|
|
101
|
+
...Object.fromEntries(Object.entries(cliConfigFields).map(([k, v]) => [k, v.schema])),
|
|
102
|
+
test: testRunnerConfigSchema.optional(),
|
|
103
|
+
});
|
|
104
|
+
function addOption(command, cli) {
|
|
105
|
+
if (cli.parseArg) {
|
|
106
|
+
command.option(cli.flags, cli.description, cli.parseArg, cli.default);
|
|
107
|
+
}
|
|
108
|
+
else if ("default" in cli && cli.default !== undefined) {
|
|
109
|
+
command.option(cli.flags, cli.description, cli.default);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
command.option(cli.flags, cli.description);
|
|
113
|
+
}
|
|
114
|
+
if (cli.negatable) {
|
|
115
|
+
const flagName = cli.flags
|
|
116
|
+
.split(" ")[0]
|
|
117
|
+
.replace(/^-+/, "")
|
|
118
|
+
.replace(/^[a-z] --/, "");
|
|
119
|
+
command.option(`--no-${flagName}`, `Disable ${flagName}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function registerAllCliOptions(command) {
|
|
123
|
+
const f = cliConfigFields;
|
|
124
|
+
command.option("-c --config <path>", "Path to config file (default: factorio-test.json, or 'factorio-test' key in package.json)");
|
|
125
|
+
command.option("-g --graphics", "Launch Factorio with graphics (interactive mode) instead of headless");
|
|
126
|
+
command.option("-w --watch", "Watch for file changes and rerun tests");
|
|
127
|
+
addOption(command, f.modPath.cli);
|
|
128
|
+
addOption(command, f.modName.cli);
|
|
129
|
+
addOption(command, f.factorioPath.cli);
|
|
130
|
+
addOption(command, f.dataDirectory.cli);
|
|
131
|
+
addOption(command, f.save.cli);
|
|
132
|
+
addOption(command, f.mods.cli);
|
|
133
|
+
addOption(command, f.factorioArgs.cli);
|
|
134
|
+
registerTestConfigOptions(command);
|
|
135
|
+
addOption(command, f.verbose.cli);
|
|
136
|
+
addOption(command, f.quiet.cli);
|
|
137
|
+
addOption(command, f.outputFile.cli);
|
|
138
|
+
command.option("--no-output-file", "Disable writing test results file");
|
|
139
|
+
addOption(command, f.forbidOnly.cli);
|
|
140
|
+
addOption(command, f.watchPatterns.cli);
|
|
141
|
+
addOption(command, f.udpPort.cli);
|
|
142
|
+
}
|
package/config/index.js
ADDED
package/config/loader.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { CliError } from "../cli-error.js";
|
|
4
|
+
import { cliConfigSchema, DEFAULT_DATA_DIRECTORY } from "./cli-config.js";
|
|
5
|
+
import { parseCliTestOptions } from "./test-config.js";
|
|
6
|
+
function formatZodError(error, filePath) {
|
|
7
|
+
const issues = error.issues.map((issue) => {
|
|
8
|
+
const pathStr = issue.path.join(".");
|
|
9
|
+
return ` - ${pathStr ? `"${pathStr}": ` : ""}${issue.message}`;
|
|
10
|
+
});
|
|
11
|
+
return `Invalid config in ${filePath}:\n${issues.join("\n")}`;
|
|
12
|
+
}
|
|
13
|
+
export function loadConfig(configPath) {
|
|
14
|
+
const paths = configPath
|
|
15
|
+
? [path.resolve(configPath)]
|
|
16
|
+
: [path.resolve("factorio-test.json"), path.resolve("package.json")];
|
|
17
|
+
for (const filePath of paths) {
|
|
18
|
+
if (!fs.existsSync(filePath))
|
|
19
|
+
continue;
|
|
20
|
+
const content = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
21
|
+
const rawConfig = filePath.endsWith("package.json") ? content["factorio-test"] : content;
|
|
22
|
+
if (!rawConfig)
|
|
23
|
+
continue;
|
|
24
|
+
const result = cliConfigSchema.strict().safeParse(rawConfig);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
throw new CliError(formatZodError(result.error, filePath));
|
|
27
|
+
}
|
|
28
|
+
return resolveConfigPaths(result.data, path.dirname(filePath));
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
function resolveConfigPaths(config, configDir) {
|
|
33
|
+
return {
|
|
34
|
+
...config,
|
|
35
|
+
modPath: config.modPath ? path.resolve(configDir, config.modPath) : undefined,
|
|
36
|
+
factorioPath: config.factorioPath ? path.resolve(configDir, config.factorioPath) : undefined,
|
|
37
|
+
dataDirectory: path.resolve(configDir, config.dataDirectory ?? DEFAULT_DATA_DIRECTORY),
|
|
38
|
+
save: config.save ? path.resolve(configDir, config.save) : undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function mergeTestConfig(configFile, cliOptions) {
|
|
42
|
+
return {
|
|
43
|
+
...configFile,
|
|
44
|
+
test_pattern: cliOptions.test_pattern ?? configFile?.test_pattern,
|
|
45
|
+
tag_whitelist: cliOptions.tag_whitelist ?? configFile?.tag_whitelist,
|
|
46
|
+
tag_blacklist: cliOptions.tag_blacklist ?? configFile?.tag_blacklist,
|
|
47
|
+
default_timeout: cliOptions.default_timeout ?? configFile?.default_timeout,
|
|
48
|
+
game_speed: cliOptions.game_speed ?? configFile?.game_speed,
|
|
49
|
+
log_passed_tests: cliOptions.log_passed_tests ?? configFile?.log_passed_tests,
|
|
50
|
+
log_skipped_tests: cliOptions.log_skipped_tests ?? configFile?.log_skipped_tests,
|
|
51
|
+
reorder_failed_first: cliOptions.reorder_failed_first ?? configFile?.reorder_failed_first,
|
|
52
|
+
bail: cliOptions.bail ?? configFile?.bail,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function mergeCliConfig(fileConfig, options) {
|
|
56
|
+
options.modPath ??= fileConfig.modPath;
|
|
57
|
+
options.modName ??= fileConfig.modName;
|
|
58
|
+
options.factorioPath ??= fileConfig.factorioPath;
|
|
59
|
+
options.dataDirectory ??= fileConfig.dataDirectory ?? DEFAULT_DATA_DIRECTORY;
|
|
60
|
+
options.mods ??= fileConfig.mods;
|
|
61
|
+
options.factorioArgs ??= fileConfig.factorioArgs;
|
|
62
|
+
options.verbose ??= fileConfig.verbose;
|
|
63
|
+
options.quiet ??= fileConfig.quiet;
|
|
64
|
+
return options;
|
|
65
|
+
}
|
|
66
|
+
export function buildTestConfig(fileConfig, options, patterns) {
|
|
67
|
+
const cliTestOptions = parseCliTestOptions(options);
|
|
68
|
+
const testPattern = patterns.length > 0
|
|
69
|
+
? patterns.map((p) => `(${p})`).join("|")
|
|
70
|
+
: (cliTestOptions.test_pattern ?? fileConfig.test?.test_pattern);
|
|
71
|
+
return mergeTestConfig(fileConfig.test, { ...cliTestOptions, test_pattern: testPattern });
|
|
72
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const testConfigFields = {
|
|
3
|
+
test_pattern: {
|
|
4
|
+
schema: z.string().optional(),
|
|
5
|
+
cli: {
|
|
6
|
+
flags: "--test-pattern <pattern>",
|
|
7
|
+
description: "Filter tests by name pattern.",
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
tag_whitelist: {
|
|
11
|
+
schema: z.array(z.string()).optional(),
|
|
12
|
+
cli: {
|
|
13
|
+
flags: "--tag-whitelist <tags...>",
|
|
14
|
+
description: "Only run tests with these tags.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
tag_blacklist: {
|
|
18
|
+
schema: z.array(z.string()).optional(),
|
|
19
|
+
cli: {
|
|
20
|
+
flags: "--tag-blacklist <tags...>",
|
|
21
|
+
description: "Skip tests with these tags.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
default_timeout: {
|
|
25
|
+
schema: z.number().int().positive().optional(),
|
|
26
|
+
cli: {
|
|
27
|
+
flags: "--default-timeout <ticks>",
|
|
28
|
+
description: "Default async test timeout in ticks.",
|
|
29
|
+
parseArg: parseInt,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
game_speed: {
|
|
33
|
+
schema: z.number().int().positive().optional(),
|
|
34
|
+
cli: {
|
|
35
|
+
flags: "--game-speed <speed>",
|
|
36
|
+
description: "Game speed multiplier.",
|
|
37
|
+
parseArg: parseInt,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
bail: {
|
|
41
|
+
schema: z.number().int().positive().optional(),
|
|
42
|
+
cli: {
|
|
43
|
+
flags: "-b --bail [count]",
|
|
44
|
+
description: "Stop after n failures (default: 1 when flag present).",
|
|
45
|
+
parseArg: (v) => (v === undefined ? 1 : parseInt(v)),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
reorder_failed_first: {
|
|
49
|
+
schema: z.boolean().optional(),
|
|
50
|
+
cli: {
|
|
51
|
+
flags: "--reorder-failed-first",
|
|
52
|
+
description: "Run previously failed tests first (default: enabled).",
|
|
53
|
+
negatable: true,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
log_passed_tests: {
|
|
57
|
+
schema: z.boolean().optional(),
|
|
58
|
+
cli: {
|
|
59
|
+
flags: "--log-passed-tests",
|
|
60
|
+
description: "Log passed test names (default: enabled).",
|
|
61
|
+
negatable: true,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
log_skipped_tests: {
|
|
65
|
+
schema: z.boolean().optional(),
|
|
66
|
+
cli: {
|
|
67
|
+
flags: "--log-skipped-tests",
|
|
68
|
+
description: "Log skipped test names.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const testRunnerConfigSchema = z.strictObject(Object.fromEntries(Object.entries(testConfigFields).map(([k, v]) => [k, v.schema])));
|
|
73
|
+
export function registerTestConfigOptions(command) {
|
|
74
|
+
for (const field of Object.values(testConfigFields)) {
|
|
75
|
+
const cli = field.cli;
|
|
76
|
+
if (cli.parseArg) {
|
|
77
|
+
command.option(cli.flags, cli.description, cli.parseArg);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
command.option(cli.flags, cli.description);
|
|
81
|
+
}
|
|
82
|
+
if (cli.negatable) {
|
|
83
|
+
const flagName = cli.flags.split(" ")[0].replace("--", "");
|
|
84
|
+
command.option(`--no-${flagName}`, `Disable ${cli.description.toLowerCase().replace(/\.$/, "")}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function snakeToCamel(str) {
|
|
89
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
90
|
+
}
|
|
91
|
+
export function parseCliTestOptions(opts) {
|
|
92
|
+
const result = {};
|
|
93
|
+
for (const snake of Object.keys(testConfigFields)) {
|
|
94
|
+
const camel = snakeToCamel(snake);
|
|
95
|
+
let value = opts[camel];
|
|
96
|
+
if (value !== undefined) {
|
|
97
|
+
if (snake === "bail" && value === true) {
|
|
98
|
+
value = 1;
|
|
99
|
+
}
|
|
100
|
+
result[snake] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factorio-test-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A CLI to run FactorioTest.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"node": ">=18.0.0"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"
|
|
18
|
+
"**/*.js",
|
|
19
19
|
"headless-save.zip"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|