gdharness 1.2.23 → 1.2.24
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/build/cli.js +103 -22
- package/build/index.js +102 -21
- package/build/keeper.js +2 -1
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -651,18 +651,18 @@ function engineFlags(given) {
|
|
|
651
651
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
652
652
|
return {
|
|
653
653
|
ok: false,
|
|
654
|
-
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
654
|
+
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
655
655
|
};
|
|
656
656
|
}
|
|
657
657
|
if (takes === "none") {
|
|
658
658
|
continue;
|
|
659
659
|
}
|
|
660
660
|
const value = given[at + 1];
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
661
|
+
const kind = VALUE_KIND[takes];
|
|
662
|
+
if (value === undefined || !kind.pattern.test(value)) {
|
|
663
663
|
return {
|
|
664
664
|
ok: false,
|
|
665
|
-
reason: `${flag} takes ${
|
|
665
|
+
reason: `${flag} takes ${kind.named} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
|
|
666
666
|
};
|
|
667
667
|
}
|
|
668
668
|
at += 1;
|
|
@@ -725,7 +725,7 @@ function environmentFor(options, variables = process.env) {
|
|
|
725
725
|
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
726
726
|
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
727
727
|
}
|
|
728
|
-
var ENGINE_FLAGS, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
728
|
+
var ENGINE_FLAGS, VALUE_KIND, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
729
729
|
var init_launch = __esm(() => {
|
|
730
730
|
ENGINE_FLAGS = Object.freeze({
|
|
731
731
|
"--verbose": "none",
|
|
@@ -746,8 +746,14 @@ var init_launch = __esm(() => {
|
|
|
746
746
|
"--time-scale": "number",
|
|
747
747
|
"--fixed-fps": "number",
|
|
748
748
|
"--max-fps": "number",
|
|
749
|
-
"--resolution": "size"
|
|
749
|
+
"--resolution": "size",
|
|
750
|
+
"--write-movie": "file"
|
|
750
751
|
});
|
|
752
|
+
VALUE_KIND = {
|
|
753
|
+
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
754
|
+
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
755
|
+
file: { pattern: /^[^-]/, named: "a file path" }
|
|
756
|
+
};
|
|
751
757
|
SET_BY_ARGUMENT = Object.freeze({
|
|
752
758
|
"--headless": "headless",
|
|
753
759
|
"--audio-driver": "silent",
|
|
@@ -11372,7 +11378,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
11372
11378
|
ignore: {
|
|
11373
11379
|
type: "array",
|
|
11374
11380
|
items: { type: "string" },
|
|
11375
|
-
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name".'
|
|
11381
|
+
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
|
|
11382
|
+
},
|
|
11383
|
+
engineArgs: {
|
|
11384
|
+
type: "array",
|
|
11385
|
+
items: { type: "string" },
|
|
11386
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
|
|
11376
11387
|
},
|
|
11377
11388
|
failFast: {
|
|
11378
11389
|
type: "boolean",
|
|
@@ -11849,7 +11860,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11849
11860
|
type: "array",
|
|
11850
11861
|
items: { type: "string" },
|
|
11851
11862
|
ops: ["start", "check"],
|
|
11852
|
-
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
11863
|
+
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
11853
11864
|
},
|
|
11854
11865
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
11855
11866
|
visible: {
|
|
@@ -39747,6 +39758,23 @@ function count(attributes, name) {
|
|
|
39747
39758
|
const value = Number(attributes[name] ?? "0");
|
|
39748
39759
|
return Number.isFinite(value) ? value : 0;
|
|
39749
39760
|
}
|
|
39761
|
+
function ignoredYetRan(report, ignored) {
|
|
39762
|
+
const found = [];
|
|
39763
|
+
for (const entry of ignored) {
|
|
39764
|
+
const bare = entry.replace("res://", "");
|
|
39765
|
+
const at = bare.lastIndexOf(":");
|
|
39766
|
+
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
39767
|
+
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
39768
|
+
for (const suite of report.suites) {
|
|
39769
|
+
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
39770
|
+
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
39771
|
+
if (ran !== undefined) {
|
|
39772
|
+
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
39773
|
+
}
|
|
39774
|
+
}
|
|
39775
|
+
}
|
|
39776
|
+
return found;
|
|
39777
|
+
}
|
|
39750
39778
|
function caseOf(suite, element) {
|
|
39751
39779
|
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
39752
39780
|
const detail = child.text.trim();
|
|
@@ -43244,29 +43272,62 @@ function restartUnderway(note, now = Date.now()) {
|
|
|
43244
43272
|
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
43245
43273
|
}
|
|
43246
43274
|
function captureDestinationRefusal(outputPath, projects) {
|
|
43247
|
-
|
|
43248
|
-
|
|
43275
|
+
return writtenFileRefusal(outputPath, {
|
|
43276
|
+
named: "outputPath",
|
|
43277
|
+
what: "a capture",
|
|
43278
|
+
extensions: [".png"],
|
|
43279
|
+
projects,
|
|
43280
|
+
writes: (where) => [where]
|
|
43281
|
+
});
|
|
43282
|
+
}
|
|
43283
|
+
function writtenFileRefusal(path, rule) {
|
|
43284
|
+
const { named, what } = rule;
|
|
43285
|
+
if (/^(res|user):\/\//.test(path)) {
|
|
43286
|
+
return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
|
|
43249
43287
|
}
|
|
43250
|
-
if (!isAbsolute3(
|
|
43251
|
-
return
|
|
43288
|
+
if (!isAbsolute3(path)) {
|
|
43289
|
+
return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
|
|
43252
43290
|
}
|
|
43253
|
-
if (extname3(
|
|
43254
|
-
return
|
|
43291
|
+
if (!rule.extensions.includes(extname3(path).toLowerCase())) {
|
|
43292
|
+
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
43255
43293
|
}
|
|
43256
|
-
const directory = realPathOr(dirname19(
|
|
43294
|
+
const directory = realPathOr(dirname19(path));
|
|
43257
43295
|
if (!existsSync20(directory)) {
|
|
43258
|
-
return
|
|
43296
|
+
return `${named}'s directory ${dirname19(path)} does not exist; ${what} creates no directories.`;
|
|
43259
43297
|
}
|
|
43260
|
-
const where = join26(directory, basename7(
|
|
43261
|
-
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
43298
|
+
const where = join26(directory, basename7(path));
|
|
43299
|
+
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
43262
43300
|
if (inside !== undefined) {
|
|
43263
|
-
return
|
|
43301
|
+
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
43264
43302
|
}
|
|
43265
|
-
|
|
43266
|
-
|
|
43303
|
+
const taken = rule.writes(where).find((file) => existsSync20(file));
|
|
43304
|
+
if (taken !== undefined) {
|
|
43305
|
+
return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
|
|
43267
43306
|
}
|
|
43268
43307
|
return null;
|
|
43269
43308
|
}
|
|
43309
|
+
function movieRefusal(engineArgs, projects, headless) {
|
|
43310
|
+
const at = engineArgs.indexOf("--write-movie");
|
|
43311
|
+
if (at === -1) {
|
|
43312
|
+
return null;
|
|
43313
|
+
}
|
|
43314
|
+
if (headless) {
|
|
43315
|
+
return "--write-movie needs a run that draws, and this one is headless: Movie Maker reads every frame back from the renderer, and the engine crashed on the first frame when asked headless. Start the run with headless: false; it is drawn on a desktop of its own and does not show.";
|
|
43316
|
+
}
|
|
43317
|
+
return writtenFileRefusal(engineArgs[at + 1] ?? "", {
|
|
43318
|
+
named: "--write-movie",
|
|
43319
|
+
what: "a movie",
|
|
43320
|
+
extensions: [".avi", ".png"],
|
|
43321
|
+
projects,
|
|
43322
|
+
writes: (where) => {
|
|
43323
|
+
if (extname3(where).toLowerCase() !== ".png") {
|
|
43324
|
+
return [where];
|
|
43325
|
+
}
|
|
43326
|
+
const base = where.slice(0, -".png".length);
|
|
43327
|
+
return [`${base}00000000.png`, `${base}.wav`];
|
|
43328
|
+
}
|
|
43329
|
+
});
|
|
43330
|
+
}
|
|
43270
43331
|
function captureDetail(payload) {
|
|
43271
43332
|
const parts = [];
|
|
43272
43333
|
const region = payload["region"];
|
|
@@ -45899,6 +45960,14 @@ class GodotServer {
|
|
|
45899
45960
|
if (!paths.ok) {
|
|
45900
45961
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
45901
45962
|
}
|
|
45963
|
+
const flags = this.engineArguments(args);
|
|
45964
|
+
if (!flags.ok) {
|
|
45965
|
+
return flags.response;
|
|
45966
|
+
}
|
|
45967
|
+
const movie = movieRefusal(flags.value, [project.value.path], true);
|
|
45968
|
+
if (movie !== null) {
|
|
45969
|
+
return this.createErrorResponse(movie);
|
|
45970
|
+
}
|
|
45902
45971
|
const missing = paths.value.filter((path) => !existsSync20(join26(project.value.path, path.slice("res://".length))));
|
|
45903
45972
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
45904
45973
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
@@ -45946,6 +46015,7 @@ class GodotServer {
|
|
|
45946
46015
|
"--headless",
|
|
45947
46016
|
"--path",
|
|
45948
46017
|
project.value.path,
|
|
46018
|
+
...flags.value,
|
|
45949
46019
|
"-s",
|
|
45950
46020
|
`res://${runner}`,
|
|
45951
46021
|
"--ignoreHeadlessMode",
|
|
@@ -46009,7 +46079,11 @@ class GodotServer {
|
|
|
46009
46079
|
rmdirSync2(dirname19(reportsDir));
|
|
46010
46080
|
} catch {}
|
|
46011
46081
|
}
|
|
46012
|
-
const engineEntries = foldedRepeats(run.log.select({
|
|
46082
|
+
const engineEntries = foldedRepeats(run.log.select({
|
|
46083
|
+
severity: flags.value.includes("--verbose") ? "info" : "warning",
|
|
46084
|
+
sinceLastCall: false,
|
|
46085
|
+
limit: Number.POSITIVE_INFINITY
|
|
46086
|
+
}).entries.map(aboveTheRunner));
|
|
46013
46087
|
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
46014
46088
|
const verdicts = {
|
|
46015
46089
|
0: "passed",
|
|
@@ -46120,6 +46194,7 @@ class GodotServer {
|
|
|
46120
46194
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
46121
46195
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
46122
46196
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
46197
|
+
const notApplied = ignoredYetRan(report, ignored);
|
|
46123
46198
|
return this.jsonTextResponse({
|
|
46124
46199
|
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
46125
46200
|
verdict,
|
|
@@ -46138,11 +46213,13 @@ class GodotServer {
|
|
|
46138
46213
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
46139
46214
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
46140
46215
|
notRun: notRun > 0 ? notRun : undefined,
|
|
46216
|
+
ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
|
|
46141
46217
|
...printedLines,
|
|
46142
46218
|
savesNote,
|
|
46143
46219
|
...scanned,
|
|
46144
46220
|
note: [
|
|
46145
46221
|
missingNote,
|
|
46222
|
+
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
46146
46223
|
uncaughtNote(uncaught),
|
|
46147
46224
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
46148
46225
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
@@ -47002,6 +47079,10 @@ class GodotServer {
|
|
|
47002
47079
|
if (!flags.ok) {
|
|
47003
47080
|
return flags.response;
|
|
47004
47081
|
}
|
|
47082
|
+
const movie = movieRefusal(flags.value, [project.value.path], op === "check" || resolveHeadless(args["headless"], { platform: process.platform, variables: process.env }));
|
|
47083
|
+
if (movie !== null) {
|
|
47084
|
+
return this.createErrorResponse(movie);
|
|
47085
|
+
}
|
|
47005
47086
|
const asked = this.runEnvironment(args);
|
|
47006
47087
|
if (!asked.ok) {
|
|
47007
47088
|
return asked.response;
|
package/build/index.js
CHANGED
|
@@ -24932,8 +24932,14 @@ var ENGINE_FLAGS = Object.freeze({
|
|
|
24932
24932
|
"--time-scale": "number",
|
|
24933
24933
|
"--fixed-fps": "number",
|
|
24934
24934
|
"--max-fps": "number",
|
|
24935
|
-
"--resolution": "size"
|
|
24935
|
+
"--resolution": "size",
|
|
24936
|
+
"--write-movie": "file"
|
|
24936
24937
|
});
|
|
24938
|
+
var VALUE_KIND = {
|
|
24939
|
+
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
24940
|
+
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
24941
|
+
file: { pattern: /^[^-]/, named: "a file path" }
|
|
24942
|
+
};
|
|
24937
24943
|
var SET_BY_ARGUMENT = Object.freeze({
|
|
24938
24944
|
"--headless": "headless",
|
|
24939
24945
|
"--audio-driver": "silent",
|
|
@@ -24948,18 +24954,18 @@ function engineFlags(given) {
|
|
|
24948
24954
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
24949
24955
|
return {
|
|
24950
24956
|
ok: false,
|
|
24951
|
-
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
24957
|
+
reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
24952
24958
|
};
|
|
24953
24959
|
}
|
|
24954
24960
|
if (takes === "none") {
|
|
24955
24961
|
continue;
|
|
24956
24962
|
}
|
|
24957
24963
|
const value = given[at + 1];
|
|
24958
|
-
const
|
|
24959
|
-
if (!
|
|
24964
|
+
const kind = VALUE_KIND[takes];
|
|
24965
|
+
if (value === undefined || !kind.pattern.test(value)) {
|
|
24960
24966
|
return {
|
|
24961
24967
|
ok: false,
|
|
24962
|
-
reason: `${flag} takes ${
|
|
24968
|
+
reason: `${flag} takes ${kind.named} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
|
|
24963
24969
|
};
|
|
24964
24970
|
}
|
|
24965
24971
|
at += 1;
|
|
@@ -29963,6 +29969,23 @@ function count(attributes, name) {
|
|
|
29963
29969
|
const value = Number(attributes[name] ?? "0");
|
|
29964
29970
|
return Number.isFinite(value) ? value : 0;
|
|
29965
29971
|
}
|
|
29972
|
+
function ignoredYetRan(report, ignored) {
|
|
29973
|
+
const found = [];
|
|
29974
|
+
for (const entry of ignored) {
|
|
29975
|
+
const bare = entry.replace("res://", "");
|
|
29976
|
+
const at = bare.lastIndexOf(":");
|
|
29977
|
+
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
29978
|
+
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
29979
|
+
for (const suite of report.suites) {
|
|
29980
|
+
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
29981
|
+
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
29982
|
+
if (ran !== undefined) {
|
|
29983
|
+
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
29984
|
+
}
|
|
29985
|
+
}
|
|
29986
|
+
}
|
|
29987
|
+
return found;
|
|
29988
|
+
}
|
|
29966
29989
|
function caseOf(suite, element) {
|
|
29967
29990
|
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
29968
29991
|
const detail = child.text.trim();
|
|
@@ -34404,7 +34427,12 @@ var TOOL_SPECS = [
|
|
|
34404
34427
|
ignore: {
|
|
34405
34428
|
type: "array",
|
|
34406
34429
|
items: { type: "string" },
|
|
34407
|
-
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name".'
|
|
34430
|
+
description: 'Suites or cases to leave out, as "suite_name" or "suite_name:test_name". A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out.'
|
|
34431
|
+
},
|
|
34432
|
+
engineArgs: {
|
|
34433
|
+
type: "array",
|
|
34434
|
+
items: { type: "string" },
|
|
34435
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
|
|
34408
34436
|
},
|
|
34409
34437
|
failFast: {
|
|
34410
34438
|
type: "boolean",
|
|
@@ -34881,7 +34909,7 @@ var TOOL_SPECS = [
|
|
|
34881
34909
|
type: "array",
|
|
34882
34910
|
items: { type: "string" },
|
|
34883
34911
|
ops: ["start", "check"],
|
|
34884
|
-
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
34912
|
+
description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames, and Movie Maker's --write-movie: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
|
|
34885
34913
|
},
|
|
34886
34914
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
34887
34915
|
visible: {
|
|
@@ -35911,29 +35939,62 @@ function restartUnderway(note, now = Date.now()) {
|
|
|
35911
35939
|
var HOLD_PING_MS = 2000;
|
|
35912
35940
|
var SLOWEST_FRAME_RATE = 20;
|
|
35913
35941
|
function captureDestinationRefusal(outputPath, projects) {
|
|
35914
|
-
|
|
35915
|
-
|
|
35942
|
+
return writtenFileRefusal(outputPath, {
|
|
35943
|
+
named: "outputPath",
|
|
35944
|
+
what: "a capture",
|
|
35945
|
+
extensions: [".png"],
|
|
35946
|
+
projects,
|
|
35947
|
+
writes: (where) => [where]
|
|
35948
|
+
});
|
|
35949
|
+
}
|
|
35950
|
+
function writtenFileRefusal(path, rule) {
|
|
35951
|
+
const { named, what } = rule;
|
|
35952
|
+
if (/^(res|user):\/\//.test(path)) {
|
|
35953
|
+
return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
|
|
35916
35954
|
}
|
|
35917
|
-
if (!isAbsolute3(
|
|
35918
|
-
return
|
|
35955
|
+
if (!isAbsolute3(path)) {
|
|
35956
|
+
return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
|
|
35919
35957
|
}
|
|
35920
|
-
if (extname3(
|
|
35921
|
-
return
|
|
35958
|
+
if (!rule.extensions.includes(extname3(path).toLowerCase())) {
|
|
35959
|
+
return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
|
|
35922
35960
|
}
|
|
35923
|
-
const directory = realPathOr(dirname15(
|
|
35961
|
+
const directory = realPathOr(dirname15(path));
|
|
35924
35962
|
if (!existsSync17(directory)) {
|
|
35925
|
-
return
|
|
35963
|
+
return `${named}'s directory ${dirname15(path)} does not exist; ${what} creates no directories.`;
|
|
35926
35964
|
}
|
|
35927
|
-
const where = join22(directory, basename5(
|
|
35928
|
-
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
35965
|
+
const where = join22(directory, basename5(path));
|
|
35966
|
+
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
35929
35967
|
if (inside !== undefined) {
|
|
35930
|
-
return
|
|
35968
|
+
return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
|
|
35931
35969
|
}
|
|
35932
|
-
|
|
35933
|
-
|
|
35970
|
+
const taken = rule.writes(where).find((file) => existsSync17(file));
|
|
35971
|
+
if (taken !== undefined) {
|
|
35972
|
+
return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
|
|
35934
35973
|
}
|
|
35935
35974
|
return null;
|
|
35936
35975
|
}
|
|
35976
|
+
function movieRefusal(engineArgs, projects, headless) {
|
|
35977
|
+
const at = engineArgs.indexOf("--write-movie");
|
|
35978
|
+
if (at === -1) {
|
|
35979
|
+
return null;
|
|
35980
|
+
}
|
|
35981
|
+
if (headless) {
|
|
35982
|
+
return "--write-movie needs a run that draws, and this one is headless: Movie Maker reads every frame back from the renderer, and the engine crashed on the first frame when asked headless. Start the run with headless: false; it is drawn on a desktop of its own and does not show.";
|
|
35983
|
+
}
|
|
35984
|
+
return writtenFileRefusal(engineArgs[at + 1] ?? "", {
|
|
35985
|
+
named: "--write-movie",
|
|
35986
|
+
what: "a movie",
|
|
35987
|
+
extensions: [".avi", ".png"],
|
|
35988
|
+
projects,
|
|
35989
|
+
writes: (where) => {
|
|
35990
|
+
if (extname3(where).toLowerCase() !== ".png") {
|
|
35991
|
+
return [where];
|
|
35992
|
+
}
|
|
35993
|
+
const base = where.slice(0, -".png".length);
|
|
35994
|
+
return [`${base}00000000.png`, `${base}.wav`];
|
|
35995
|
+
}
|
|
35996
|
+
});
|
|
35997
|
+
}
|
|
35937
35998
|
function captureDetail(payload) {
|
|
35938
35999
|
const parts = [];
|
|
35939
36000
|
const region = payload["region"];
|
|
@@ -38647,6 +38708,14 @@ class GodotServer {
|
|
|
38647
38708
|
if (!paths.ok) {
|
|
38648
38709
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
38649
38710
|
}
|
|
38711
|
+
const flags = this.engineArguments(args);
|
|
38712
|
+
if (!flags.ok) {
|
|
38713
|
+
return flags.response;
|
|
38714
|
+
}
|
|
38715
|
+
const movie = movieRefusal(flags.value, [project.value.path], true);
|
|
38716
|
+
if (movie !== null) {
|
|
38717
|
+
return this.createErrorResponse(movie);
|
|
38718
|
+
}
|
|
38650
38719
|
const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
|
|
38651
38720
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
38652
38721
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
@@ -38694,6 +38763,7 @@ class GodotServer {
|
|
|
38694
38763
|
"--headless",
|
|
38695
38764
|
"--path",
|
|
38696
38765
|
project.value.path,
|
|
38766
|
+
...flags.value,
|
|
38697
38767
|
"-s",
|
|
38698
38768
|
`res://${runner}`,
|
|
38699
38769
|
"--ignoreHeadlessMode",
|
|
@@ -38757,7 +38827,11 @@ class GodotServer {
|
|
|
38757
38827
|
rmdirSync2(dirname15(reportsDir));
|
|
38758
38828
|
} catch {}
|
|
38759
38829
|
}
|
|
38760
|
-
const engineEntries = foldedRepeats(run.log.select({
|
|
38830
|
+
const engineEntries = foldedRepeats(run.log.select({
|
|
38831
|
+
severity: flags.value.includes("--verbose") ? "info" : "warning",
|
|
38832
|
+
sinceLastCall: false,
|
|
38833
|
+
limit: Number.POSITIVE_INFINITY
|
|
38834
|
+
}).entries.map(aboveTheRunner));
|
|
38761
38835
|
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
38762
38836
|
const verdicts = {
|
|
38763
38837
|
0: "passed",
|
|
@@ -38868,6 +38942,7 @@ class GodotServer {
|
|
|
38868
38942
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
38869
38943
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
38870
38944
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
38945
|
+
const notApplied = ignoredYetRan(report, ignored);
|
|
38871
38946
|
return this.jsonTextResponse({
|
|
38872
38947
|
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && missing.length === 0,
|
|
38873
38948
|
verdict,
|
|
@@ -38886,11 +38961,13 @@ class GodotServer {
|
|
|
38886
38961
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
38887
38962
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
38888
38963
|
notRun: notRun > 0 ? notRun : undefined,
|
|
38964
|
+
ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
|
|
38889
38965
|
...printedLines,
|
|
38890
38966
|
savesNote,
|
|
38891
38967
|
...scanned,
|
|
38892
38968
|
note: [
|
|
38893
38969
|
missingNote,
|
|
38970
|
+
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
38894
38971
|
uncaughtNote(uncaught),
|
|
38895
38972
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
38896
38973
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
@@ -39750,6 +39827,10 @@ class GodotServer {
|
|
|
39750
39827
|
if (!flags.ok) {
|
|
39751
39828
|
return flags.response;
|
|
39752
39829
|
}
|
|
39830
|
+
const movie = movieRefusal(flags.value, [project.value.path], op === "check" || resolveHeadless(args["headless"], { platform: process.platform, variables: process.env }));
|
|
39831
|
+
if (movie !== null) {
|
|
39832
|
+
return this.createErrorResponse(movie);
|
|
39833
|
+
}
|
|
39753
39834
|
const asked = this.runEnvironment(args);
|
|
39754
39835
|
if (!asked.ok) {
|
|
39755
39836
|
return asked.response;
|
package/build/keeper.js
CHANGED
|
@@ -382,7 +382,8 @@ var ENGINE_FLAGS = Object.freeze({
|
|
|
382
382
|
"--time-scale": "number",
|
|
383
383
|
"--fixed-fps": "number",
|
|
384
384
|
"--max-fps": "number",
|
|
385
|
-
"--resolution": "size"
|
|
385
|
+
"--resolution": "size",
|
|
386
|
+
"--write-movie": "file"
|
|
386
387
|
});
|
|
387
388
|
var SET_BY_ARGUMENT = Object.freeze({
|
|
388
389
|
"--headless": "headless",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.24",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|