gdharness 1.2.22 → 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 +112 -26
- package/build/godot/operations/dependencies.gd +14 -0
- package/build/godot/operations/gdscript_analysis.gd +2 -24
- package/build/godot/operations/patterns.gd +22 -0
- package/build/index.js +110 -24
- 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",
|
|
@@ -11422,7 +11433,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11422
11433
|
name: "scene_tree",
|
|
11423
11434
|
title: "Scene tree",
|
|
11424
11435
|
effect: "read",
|
|
11425
|
-
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected.",
|
|
11436
|
+
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected. For the nodes of a game that is running, runtime_inspect tree, which needs no editor.",
|
|
11426
11437
|
parameters: {
|
|
11427
11438
|
projectPath: PROJECT_PATH,
|
|
11428
11439
|
scenePath: SCENE_PATH,
|
|
@@ -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"];
|
|
@@ -44893,7 +44954,7 @@ class GodotServer {
|
|
|
44893
44954
|
const { op: _op, ...arguments_ } = args;
|
|
44894
44955
|
const bridge = async (command, extra = {}) => {
|
|
44895
44956
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
44896
|
-
return contained.ok ? await this.handleViaBridge(command, contained.value) : contained.response;
|
|
44957
|
+
return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
|
|
44897
44958
|
};
|
|
44898
44959
|
switch (tool) {
|
|
44899
44960
|
case "project_info":
|
|
@@ -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;
|
|
@@ -49101,9 +49182,11 @@ class GodotServer {
|
|
|
49101
49182
|
const holds = held["classes"].map(String);
|
|
49102
49183
|
return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
|
|
49103
49184
|
}
|
|
49104
|
-
async handleViaBridge(toolName, args) {
|
|
49185
|
+
async handleViaBridge(toolName, args, askedAs) {
|
|
49105
49186
|
if (!this.godotBridge.isConnected()) {
|
|
49187
|
+
const instead = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
|
|
49106
49188
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
49189
|
+
...instead === undefined ? [] : [instead],
|
|
49107
49190
|
"Open the project in the editor with editor_launch, or by hand",
|
|
49108
49191
|
"Enable the addon under Project > Project Settings > Plugins",
|
|
49109
49192
|
"editor_status says whether the bridge is up and what it is listening on"
|
|
@@ -49416,7 +49499,7 @@ async function runGodotServer() {
|
|
|
49416
49499
|
const server = new GodotServer;
|
|
49417
49500
|
await server.run();
|
|
49418
49501
|
}
|
|
49419
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
49502
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, EDITOR_FREE_INSTEAD, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
49420
49503
|
var init_server = __esm(() => {
|
|
49421
49504
|
init_dist();
|
|
49422
49505
|
init_stdio();
|
|
@@ -49480,6 +49563,9 @@ var init_server = __esm(() => {
|
|
|
49480
49563
|
object: "an object"
|
|
49481
49564
|
};
|
|
49482
49565
|
HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
49566
|
+
EDITOR_FREE_INSTEAD = {
|
|
49567
|
+
scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
|
|
49568
|
+
};
|
|
49483
49569
|
OPS_ELSEWHERE = {
|
|
49484
49570
|
runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
|
|
49485
49571
|
};
|
|
@@ -284,6 +284,15 @@ static func _code_of(line: String) -> String:
|
|
|
284
284
|
return bare if mark == -1 else bare.substr(0, mark)
|
|
285
285
|
|
|
286
286
|
|
|
287
|
+
# [param content] with each line's comment taken off and its strings kept, which is where the paths
|
|
288
|
+
# a script loads are written.
|
|
289
|
+
static func _code_lines(content: String) -> String:
|
|
290
|
+
var kept: Array[String] = []
|
|
291
|
+
for line: String in content.split("\n"):
|
|
292
|
+
kept.append(Patterns.without_comment(line))
|
|
293
|
+
return "\n".join(kept)
|
|
294
|
+
|
|
295
|
+
|
|
287
296
|
# The comment on [param line], strings emptied first so a # inside one does not start it.
|
|
288
297
|
static func _comment_of(line: String) -> String:
|
|
289
298
|
var bare: String = Patterns.compiled(STRING_LITERAL).sub(line, '""', true)
|
|
@@ -361,6 +370,11 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
|
|
|
361
370
|
if file:
|
|
362
371
|
var content: String = file.get_as_text()
|
|
363
372
|
file.close()
|
|
373
|
+
# A script's comments name paths as examples, and read as references they were answered as
|
|
374
|
+
# dependencies, one of a file that does not exist: the shipped serialisation module's comment
|
|
375
|
+
# about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the addon.
|
|
376
|
+
if path.get_extension() == "gd":
|
|
377
|
+
content = _code_lines(content)
|
|
364
378
|
|
|
365
379
|
for pattern: String in REFERENCE_PATTERNS:
|
|
366
380
|
var regex: RegEx = Patterns.compiled(pattern)
|
|
@@ -59,7 +59,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
59
59
|
if _triple_quotes(raw) % 2 == 1:
|
|
60
60
|
in_multiline_string = false
|
|
61
61
|
continue
|
|
62
|
-
var stripped: String =
|
|
62
|
+
var stripped: String = Patterns.without_comment(raw)
|
|
63
63
|
|
|
64
64
|
if stripped.is_empty():
|
|
65
65
|
continue
|
|
@@ -101,7 +101,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
101
101
|
if _can_wrap(header):
|
|
102
102
|
while _bracket_depth(stripped) > 0 and i + 1 < lines.size():
|
|
103
103
|
i += 1
|
|
104
|
-
stripped = (stripped + " " +
|
|
104
|
+
stripped = (stripped + " " + Patterns.without_comment(lines[i])).strip_edges()
|
|
105
105
|
header = Patterns.without_annotations(stripped)
|
|
106
106
|
var declaration: RegExMatch = Patterns.declared_class(header)
|
|
107
107
|
if declaration != null:
|
|
@@ -508,28 +508,6 @@ func _up_to(text: String, needle: String) -> String:
|
|
|
508
508
|
return text
|
|
509
509
|
|
|
510
510
|
|
|
511
|
-
# The line with any trailing comment taken off, quote-aware so a `#` inside a string stays.
|
|
512
|
-
#
|
|
513
|
-
# The only one of these scanners that does not count brackets, on purpose: a `#` starts a comment
|
|
514
|
-
# wherever it is, and there is no depth at which it does not. Giving this one the depth its
|
|
515
|
-
# neighbours have puts the comment into the value of `var table := { # keyed by name`, because the
|
|
516
|
-
# brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
|
|
517
|
-
# difference is not visible from the code and reads like an oversight beside its neighbours.
|
|
518
|
-
func _without_comment(line: String) -> String:
|
|
519
|
-
var quote: String = ""
|
|
520
|
-
for i: int in range(line.length()):
|
|
521
|
-
var ch: String = line[i]
|
|
522
|
-
if not quote.is_empty():
|
|
523
|
-
if ch == quote and line[i - 1] != "\\":
|
|
524
|
-
quote = ""
|
|
525
|
-
continue
|
|
526
|
-
if ch == "#":
|
|
527
|
-
return line.substr(0, i).strip_edges()
|
|
528
|
-
if ch == '"' or ch == "'":
|
|
529
|
-
quote = ch
|
|
530
|
-
return line.strip_edges()
|
|
531
|
-
|
|
532
|
-
|
|
533
511
|
# Where the bracket opened at [from] closes, or -1 if it does not.
|
|
534
512
|
#
|
|
535
513
|
# `rfind(")")` took the last one on the line, which is the right answer until something after the
|
|
@@ -84,3 +84,25 @@ static func without_annotations(line: String) -> String:
|
|
|
84
84
|
rest = rest.substr(found.get_end()).strip_edges()
|
|
85
85
|
found = annotation.search(rest)
|
|
86
86
|
return rest
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# The line with any trailing comment taken off, quote-aware so a `#` inside a string stays.
|
|
90
|
+
#
|
|
91
|
+
# The only one of the scanners here that does not count brackets, on purpose: a `#` starts a comment
|
|
92
|
+
# wherever it is, and there is no depth at which it does not. Giving this one the depth its
|
|
93
|
+
# neighbours have puts the comment into the value of `var table := { # keyed by name`, because the
|
|
94
|
+
# brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
|
|
95
|
+
# difference is not visible from the code and reads like an oversight beside its neighbours.
|
|
96
|
+
static func without_comment(line: String) -> String:
|
|
97
|
+
var quote: String = ""
|
|
98
|
+
for i: int in range(line.length()):
|
|
99
|
+
var ch: String = line[i]
|
|
100
|
+
if not quote.is_empty():
|
|
101
|
+
if ch == quote and line[i - 1] != "\\":
|
|
102
|
+
quote = ""
|
|
103
|
+
continue
|
|
104
|
+
if ch == "#":
|
|
105
|
+
return line.substr(0, i).strip_edges()
|
|
106
|
+
if ch == '"' or ch == "'":
|
|
107
|
+
quote = ch
|
|
108
|
+
return line.strip_edges()
|
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",
|
|
@@ -34454,7 +34482,7 @@ var TOOL_SPECS = [
|
|
|
34454
34482
|
name: "scene_tree",
|
|
34455
34483
|
title: "Scene tree",
|
|
34456
34484
|
effect: "read",
|
|
34457
|
-
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected.",
|
|
34485
|
+
description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. Needs the editor connected. For the nodes of a game that is running, runtime_inspect tree, which needs no editor.",
|
|
34458
34486
|
parameters: {
|
|
34459
34487
|
projectPath: PROJECT_PATH2,
|
|
34460
34488
|
scenePath: SCENE_PATH,
|
|
@@ -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"];
|
|
@@ -36448,6 +36509,9 @@ function engineFlagsNote(args) {
|
|
|
36448
36509
|
const named = among.join(", ");
|
|
36449
36510
|
return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
|
|
36450
36511
|
}
|
|
36512
|
+
var EDITOR_FREE_INSTEAD = {
|
|
36513
|
+
scene_tree: "runtime_inspect tree reads the nodes of a game that is running, and needs no editor; scene_tree reads a scene file through the editor"
|
|
36514
|
+
};
|
|
36451
36515
|
var OPS_ELSEWHERE = {
|
|
36452
36516
|
runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
|
|
36453
36517
|
};
|
|
@@ -37638,7 +37702,7 @@ class GodotServer {
|
|
|
37638
37702
|
const { op: _op, ...arguments_ } = args;
|
|
37639
37703
|
const bridge = async (command, extra = {}) => {
|
|
37640
37704
|
const contained = this.containProjectFiles({ ...arguments_, ...extra });
|
|
37641
|
-
return contained.ok ? await this.handleViaBridge(command, contained.value) : contained.response;
|
|
37705
|
+
return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
|
|
37642
37706
|
};
|
|
37643
37707
|
switch (tool) {
|
|
37644
37708
|
case "project_info":
|
|
@@ -38644,6 +38708,14 @@ class GodotServer {
|
|
|
38644
38708
|
if (!paths.ok) {
|
|
38645
38709
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
38646
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
|
+
}
|
|
38647
38719
|
const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
|
|
38648
38720
|
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
38649
38721
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
@@ -38691,6 +38763,7 @@ class GodotServer {
|
|
|
38691
38763
|
"--headless",
|
|
38692
38764
|
"--path",
|
|
38693
38765
|
project.value.path,
|
|
38766
|
+
...flags.value,
|
|
38694
38767
|
"-s",
|
|
38695
38768
|
`res://${runner}`,
|
|
38696
38769
|
"--ignoreHeadlessMode",
|
|
@@ -38754,7 +38827,11 @@ class GodotServer {
|
|
|
38754
38827
|
rmdirSync2(dirname15(reportsDir));
|
|
38755
38828
|
} catch {}
|
|
38756
38829
|
}
|
|
38757
|
-
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));
|
|
38758
38835
|
const engineEntriesShown = engineEntries.slice(-MOST_ENGINE_ENTRIES);
|
|
38759
38836
|
const verdicts = {
|
|
38760
38837
|
0: "passed",
|
|
@@ -38865,6 +38942,7 @@ class GodotServer {
|
|
|
38865
38942
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
38866
38943
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
38867
38944
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
38945
|
+
const notApplied = ignoredYetRan(report, ignored);
|
|
38868
38946
|
return this.jsonTextResponse({
|
|
38869
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,
|
|
38870
38948
|
verdict,
|
|
@@ -38883,11 +38961,13 @@ class GodotServer {
|
|
|
38883
38961
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
38884
38962
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
38885
38963
|
notRun: notRun > 0 ? notRun : undefined,
|
|
38964
|
+
ignoreNotApplied: notApplied.length > 0 ? notApplied : undefined,
|
|
38886
38965
|
...printedLines,
|
|
38887
38966
|
savesNote,
|
|
38888
38967
|
...scanned,
|
|
38889
38968
|
note: [
|
|
38890
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,
|
|
38891
38971
|
uncaughtNote(uncaught),
|
|
38892
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)
|
|
38893
38973
|
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
@@ -39747,6 +39827,10 @@ class GodotServer {
|
|
|
39747
39827
|
if (!flags.ok) {
|
|
39748
39828
|
return flags.response;
|
|
39749
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
|
+
}
|
|
39750
39834
|
const asked = this.runEnvironment(args);
|
|
39751
39835
|
if (!asked.ok) {
|
|
39752
39836
|
return asked.response;
|
|
@@ -41846,9 +41930,11 @@ class GodotServer {
|
|
|
41846
41930
|
const holds = held["classes"].map(String);
|
|
41847
41931
|
return { unseen: unseenByEditor(projectPath, holds), stillHeld: heldButGone(projectPath, holds) };
|
|
41848
41932
|
}
|
|
41849
|
-
async handleViaBridge(toolName, args) {
|
|
41933
|
+
async handleViaBridge(toolName, args, askedAs) {
|
|
41850
41934
|
if (!this.godotBridge.isConnected()) {
|
|
41935
|
+
const instead = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
|
|
41851
41936
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
41937
|
+
...instead === undefined ? [] : [instead],
|
|
41852
41938
|
"Open the project in the editor with editor_launch, or by hand",
|
|
41853
41939
|
"Enable the addon under Project > Project Settings > Plugins",
|
|
41854
41940
|
"editor_status says whether the bridge is up and what it is listening on"
|
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",
|