gdharness 1.3.28 → 1.3.30
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
CHANGED
|
@@ -10176,6 +10176,36 @@ function walkStoppedPartWay(lines) {
|
|
|
10176
10176
|
}
|
|
10177
10177
|
return began;
|
|
10178
10178
|
}
|
|
10179
|
+
function heldAtExit(lines) {
|
|
10180
|
+
const objects = new Map;
|
|
10181
|
+
const inUse = [];
|
|
10182
|
+
for (const line of lines) {
|
|
10183
|
+
const object = LEAKED_INSTANCE.exec(line.trim())?.[1];
|
|
10184
|
+
if (object !== undefined) {
|
|
10185
|
+
objects.set(object, (objects.get(object) ?? 0) + 1);
|
|
10186
|
+
}
|
|
10187
|
+
const resource = STILL_IN_USE.exec(line.trim())?.[1];
|
|
10188
|
+
if (resource !== undefined) {
|
|
10189
|
+
inUse.push(resource);
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
10192
|
+
if (objects.size === 0 && inUse.length === 0) {
|
|
10193
|
+
return;
|
|
10194
|
+
}
|
|
10195
|
+
const counted = [...objects].sort(([a, many], [b, more]) => more - many || a.localeCompare(b));
|
|
10196
|
+
const objectsSaid = counted.map(([name, many]) => `${String(many)} ${name}`);
|
|
10197
|
+
const ownFirst = [
|
|
10198
|
+
...inUse.filter((path) => !path.startsWith("res://addons/")),
|
|
10199
|
+
...inUse.filter((path) => path.startsWith("res://addons/"))
|
|
10200
|
+
];
|
|
10201
|
+
const named = ownFirst.slice(0, IN_USE_NAMED);
|
|
10202
|
+
const more = ownFirst.length - named.length;
|
|
10203
|
+
const parts = [
|
|
10204
|
+
objectsSaid.length === 0 ? undefined : `objects left alive: ${objectsSaid.join(", ")}`,
|
|
10205
|
+
named.length === 0 ? undefined : `resources still in use: ${named.join(", ")}${more > 0 ? `, and ${String(more)} more` : ""}`
|
|
10206
|
+
].filter((part) => part !== undefined);
|
|
10207
|
+
return `The engine named them: ${parts.join("; ")}.`;
|
|
10208
|
+
}
|
|
10179
10209
|
function selfTypedExports(sources) {
|
|
10180
10210
|
const found = [];
|
|
10181
10211
|
for (const [script, source] of sources) {
|
|
@@ -10208,11 +10238,13 @@ function selfTypedSentence(found) {
|
|
|
10208
10238
|
const one = found.length === 1;
|
|
10209
10239
|
return `Godot never frees a script exporting an array or dictionary typed with its own class once a run ` + `has loaded it, and reports it as leaked at exit: ${named} ${one ? "does" : "do"}, so a run loading ` + `${one ? "it" : "one"} ends with this report whatever else it frees. Typed with a class it extends ` + `instead, as Array[Resource], ${one ? "it is" : "each is"} freed.`;
|
|
10210
10240
|
}
|
|
10211
|
-
var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scripts", WALK_ENDED = "gdharness: the engine's shutdown cleared every script", WALK_STOPPED_SENTENCE, NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*", EXPORTED;
|
|
10241
|
+
var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scripts", WALK_ENDED = "gdharness: the engine's shutdown cleared every script", LEAKED_INSTANCE, STILL_IN_USE, IN_USE_NAMED = 8, WALK_STOPPED_SENTENCE, NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*", EXPORTED;
|
|
10212
10242
|
var init_engine_leaks = __esm(() => {
|
|
10213
10243
|
init_class_cache();
|
|
10214
10244
|
init_gdscript_source();
|
|
10215
10245
|
EXIT_LEAK = /ObjectDB instances? (?:was |were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit|RIDs? of type ["'][^"']+["'] (?:was|were) leaked/;
|
|
10246
|
+
LEAKED_INSTANCE = /^Leaked instance: (\w+):/;
|
|
10247
|
+
STILL_IN_USE = /^Resource still in use: (.+?) \(\w+\)$/;
|
|
10216
10248
|
WALK_STOPPED_SENTENCE = "The engine's shutdown stopped part way through its scripts (godot#117991, fixed in 4.8 by " + "godot#120976), so it reported as leaked at exit what it never got to free, and the report is the " + "engine's rather than the run's. gdharness holds every script loaded from a file past that walk, " + "which keeps it whole, so this one stopped on a script it could not hold, such as one made in code. " + "A leak of the run's own is not told apart in such a run.";
|
|
10217
10249
|
EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
|
|
10218
10250
|
});
|
|
@@ -11104,9 +11136,11 @@ function judgeEngine(printed, handed, end) {
|
|
|
11104
11136
|
if (printed.leaks.length > 0 && walkStoppedPartWay(printed.lines)) {
|
|
11105
11137
|
notes.push(`${WALK_STOPPED_SENTENCE} It said: ${printed.leaks.join(" ")}`);
|
|
11106
11138
|
} else {
|
|
11139
|
+
const held = heldAtExit(printed.lines);
|
|
11107
11140
|
printed.leaks.forEach((leak, at) => {
|
|
11108
11141
|
const last = at === printed.leaks.length - 1;
|
|
11109
|
-
|
|
11142
|
+
const after = last ? [held, selfTyped].filter((part) => part !== undefined) : [];
|
|
11143
|
+
faults.push(`the engine exited holding what it should have freed: ${[leak, ...after].join(" ")}`);
|
|
11110
11144
|
});
|
|
11111
11145
|
}
|
|
11112
11146
|
const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
|
|
@@ -11734,12 +11768,9 @@ class EngineSlots {
|
|
|
11734
11768
|
try {
|
|
11735
11769
|
for (;; ) {
|
|
11736
11770
|
signal?.throwIfAborted();
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
this.held.add(lock);
|
|
11741
|
-
return lock;
|
|
11742
|
-
}
|
|
11771
|
+
const lock = await this.takeFree();
|
|
11772
|
+
if (lock !== null) {
|
|
11773
|
+
return lock;
|
|
11743
11774
|
}
|
|
11744
11775
|
mark ??= this.waitMarked();
|
|
11745
11776
|
const now = Date.now();
|
|
@@ -11760,6 +11791,17 @@ class EngineSlots {
|
|
|
11760
11791
|
}
|
|
11761
11792
|
}
|
|
11762
11793
|
}
|
|
11794
|
+
async takeFree() {
|
|
11795
|
+
mkdirSync3(this.directory, { recursive: true });
|
|
11796
|
+
for (let slot = 0;slot < this.count; slot += 1) {
|
|
11797
|
+
const lock = join8(this.directory, `${String(slot)}.lock`);
|
|
11798
|
+
if (this.made(lock) || await this.takenOver(lock)) {
|
|
11799
|
+
this.held.add(lock);
|
|
11800
|
+
return lock;
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11803
|
+
return null;
|
|
11804
|
+
}
|
|
11763
11805
|
waitMarked() {
|
|
11764
11806
|
const marks = join8(this.directory, WAITING_FOLDER);
|
|
11765
11807
|
waitsMarked += 1;
|
|
@@ -12072,6 +12114,37 @@ function editorArguments(projectPath, ports, logFile, hidden, silent) {
|
|
|
12072
12114
|
logFile
|
|
12073
12115
|
];
|
|
12074
12116
|
}
|
|
12117
|
+
function startedAs(commandLine) {
|
|
12118
|
+
const words = commandLineWords(commandLine);
|
|
12119
|
+
const hidden = words.includes("--headless");
|
|
12120
|
+
const driver = words.indexOf("--audio-driver");
|
|
12121
|
+
return { hidden, silent: hidden || driver >= 0 && words[driver + 1] === "Dummy" };
|
|
12122
|
+
}
|
|
12123
|
+
function commandLineWords(line) {
|
|
12124
|
+
const words = [];
|
|
12125
|
+
let word = "";
|
|
12126
|
+
let inWord = false;
|
|
12127
|
+
let quoted = false;
|
|
12128
|
+
for (const character of line) {
|
|
12129
|
+
if (character === '"') {
|
|
12130
|
+
quoted = !quoted;
|
|
12131
|
+
inWord = true;
|
|
12132
|
+
} else if (!quoted && /\s/.test(character)) {
|
|
12133
|
+
if (inWord) {
|
|
12134
|
+
words.push(word);
|
|
12135
|
+
}
|
|
12136
|
+
word = "";
|
|
12137
|
+
inWord = false;
|
|
12138
|
+
} else {
|
|
12139
|
+
word += character;
|
|
12140
|
+
inWord = true;
|
|
12141
|
+
}
|
|
12142
|
+
}
|
|
12143
|
+
if (inWord) {
|
|
12144
|
+
words.push(word);
|
|
12145
|
+
}
|
|
12146
|
+
return words;
|
|
12147
|
+
}
|
|
12075
12148
|
function userDataIn(home, variables = process.env) {
|
|
12076
12149
|
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
12077
12150
|
const carried = {};
|
|
@@ -17229,19 +17302,19 @@ var init_tool_definitions = __esm(() => {
|
|
|
17229
17302
|
{
|
|
17230
17303
|
name: "editor_launch",
|
|
17231
17304
|
title: "Open, restart or close the editor, or bind its debug adapter again",
|
|
17232
|
-
description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, visible unless hidden
|
|
17233
|
-
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
|
|
17305
|
+
description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; an editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import; a call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out. An editor a server opened is started again by the server, hidden or visible and silent or not as it was started unless hidden or silent says otherwise, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it; a debug session open on the adapter ends with it and is opened again by the next debug call. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
|
|
17306
|
+
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell; an editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had; a scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and hidden, whether it came back with no window, and says under settingsDropped which keys its save dropped, because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took; neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
|
|
17234
17307
|
parameters: {
|
|
17235
17308
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
17236
17309
|
hidden: {
|
|
17237
17310
|
type: "boolean",
|
|
17238
17311
|
ops: ["open", "restart"],
|
|
17239
|
-
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
|
|
17312
|
+
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false on open: one visible editor, shown without taking the focus. On restart only for an editor a server opened, where the default is how the editor being replaced was started, read from its process; one whose start cannot be read is left running and refused until hidden is passed. One opened by hand comes back as it was, so hidden is refused there."
|
|
17240
17313
|
},
|
|
17241
17314
|
silent: {
|
|
17242
17315
|
type: "boolean",
|
|
17243
17316
|
ops: ["open", "restart"],
|
|
17244
|
-
description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is headless and silent either way. On restart only for an editor a server opened; one opened by hand comes back as it was."
|
|
17317
|
+
description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true on open, and on restart how the editor being replaced was started: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is headless and silent either way. On restart only for an editor a server opened; one opened by hand comes back as it was."
|
|
17245
17318
|
}
|
|
17246
17319
|
},
|
|
17247
17320
|
requires: [],
|
|
@@ -17418,7 +17491,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17418
17491
|
requires: []
|
|
17419
17492
|
}
|
|
17420
17493
|
},
|
|
17421
|
-
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The functions of gdharness's own runtime are not among them: the runtime addon's, which answer its tools inside the game, and those of res://gdharness/loader.gd, which starts it. Their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
17494
|
+
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. runtime carries addonIsStale and staleNote when the game announced on a runtime addon other than this server's, which no runtime_* call is then sent to, and staleNote says what brings it into step. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). exitedBeforeStop is true when the run was already over when the stop came, so nothing was ended and exitCode, when anything collected it, is the one it ended with. A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is, and durationMs runs from that engine's start to the check's answer, after its exit, so it holds no import that came first. start and check: " + IMPORTED_FIRST + " An exported build carries its own imports and is not asked. waitedForEditorScanMs and scanNote are left out when no editor scan was waited for. A silent windowed run has --audio-driver Dummy in its arguments; a headless run is silent without it, and one the editor plays with sound has no arguments of its own. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The functions of gdharness's own runtime are not among them: the runtime addon's, which answer its tools inside the game, and those of res://gdharness/loader.gd, which starts it. Their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. firstFrame and lastFrame are the game's own numbers, counted from 0, for the first and the last of them, in a run this server started; a firstFrame above 0 is frames run before the profiler came on, and a gap between the two is frames that never arrived, which the note says. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
17422
17495
|
defaultOperation: "start"
|
|
17423
17496
|
},
|
|
17424
17497
|
{
|
|
@@ -17662,7 +17735,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17662
17735
|
},
|
|
17663
17736
|
find: {
|
|
17664
17737
|
effect: "read",
|
|
17665
|
-
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise; with it false, a control drawn wholly outside the viewport or a ScrollContainer is left out too. A match drawn under a matching node
|
|
17738
|
+
summary: "the paths of every node matching className, script, namePattern, group or says, with property read off each, hidden ones included unless includeHidden says otherwise; with it false, a control drawn wholly outside the viewport or a ScrollContainer is left out too. A match drawn under a matching node drawing the same words, as an outline or a glow copy is, names that node under copyOf (a name given only for a screen reader is not drawn, so a node holding one is never what a copy copies), and the answer counts the rest under distinct",
|
|
17666
17739
|
requires: []
|
|
17667
17740
|
},
|
|
17668
17741
|
rect: {
|
|
@@ -18607,7 +18680,7 @@ function dealt(units, declared, most) {
|
|
|
18607
18680
|
return [];
|
|
18608
18681
|
}
|
|
18609
18682
|
const windowed = units.filter((unit) => unit.windowed);
|
|
18610
|
-
const headless = units.filter((unit) => !unit.windowed).sort(
|
|
18683
|
+
const headless = units.filter((unit) => !unit.windowed).sort(slowestFirst);
|
|
18611
18684
|
const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
|
|
18612
18685
|
const restart = windowed.length > 0 ? ENGINE_START_SECONDS : 0;
|
|
18613
18686
|
const total = units.reduce((sum, unit) => sum + unit.cost, 0);
|
|
@@ -18625,6 +18698,52 @@ function dealt(units, declared, most) {
|
|
|
18625
18698
|
...bins.flatMap((bin, lane) => bin.length === 0 ? [] : [planOf(bin, declared, false, lane, units)])
|
|
18626
18699
|
];
|
|
18627
18700
|
}
|
|
18701
|
+
function slowestFirst(a, b) {
|
|
18702
|
+
return b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? "") || (a.row?.index ?? 0) - (b.row?.index ?? 0);
|
|
18703
|
+
}
|
|
18704
|
+
|
|
18705
|
+
class Shares {
|
|
18706
|
+
windowed;
|
|
18707
|
+
headless;
|
|
18708
|
+
declared;
|
|
18709
|
+
all;
|
|
18710
|
+
lanes;
|
|
18711
|
+
constructor(units, declared, lanes) {
|
|
18712
|
+
this.windowed = units.filter((unit) => unit.windowed);
|
|
18713
|
+
this.headless = units.filter((unit) => !unit.windowed).sort(slowestFirst);
|
|
18714
|
+
this.declared = declared;
|
|
18715
|
+
this.all = units;
|
|
18716
|
+
this.lanes = Math.max(1, lanes);
|
|
18717
|
+
}
|
|
18718
|
+
get empty() {
|
|
18719
|
+
return this.windowed.length === 0 && this.headless.length === 0;
|
|
18720
|
+
}
|
|
18721
|
+
next(lane) {
|
|
18722
|
+
if (this.windowed.length > 0) {
|
|
18723
|
+
const windowed = this.windowed;
|
|
18724
|
+
this.windowed = [];
|
|
18725
|
+
return planOf(windowed, this.declared, true, lane, this.all);
|
|
18726
|
+
}
|
|
18727
|
+
if (this.headless.length === 0) {
|
|
18728
|
+
return null;
|
|
18729
|
+
}
|
|
18730
|
+
const left = this.headless.reduce((sum, unit) => sum + unit.cost, 0);
|
|
18731
|
+
const target = Math.max(SHARE_FLOOR_SECONDS, left / this.lanes);
|
|
18732
|
+
const share = [];
|
|
18733
|
+
const rest = [];
|
|
18734
|
+
let cost = 0;
|
|
18735
|
+
for (const unit of this.headless) {
|
|
18736
|
+
if (share.length === 0 || cost + unit.cost <= target) {
|
|
18737
|
+
share.push(unit);
|
|
18738
|
+
cost += unit.cost;
|
|
18739
|
+
} else {
|
|
18740
|
+
rest.push(unit);
|
|
18741
|
+
}
|
|
18742
|
+
}
|
|
18743
|
+
this.headless = rest;
|
|
18744
|
+
return planOf(share, this.declared, false, lane, this.all);
|
|
18745
|
+
}
|
|
18746
|
+
}
|
|
18628
18747
|
function lanesOf(plans) {
|
|
18629
18748
|
return new Set(plans.map((plan) => plan.lane)).size;
|
|
18630
18749
|
}
|
|
@@ -18695,7 +18814,7 @@ function viewportOf(projectPath) {
|
|
|
18695
18814
|
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
18696
18815
|
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
18697
18816
|
}
|
|
18698
|
-
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5;
|
|
18817
|
+
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
|
|
18699
18818
|
var init_suite_plan = __esm(() => {
|
|
18700
18819
|
init_class_cache();
|
|
18701
18820
|
init_gdscript_source();
|
|
@@ -18704,6 +18823,7 @@ var init_suite_plan = __esm(() => {
|
|
|
18704
18823
|
NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
|
|
18705
18824
|
TEST_DIRECTORIES = ["tests", "test"];
|
|
18706
18825
|
SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
18826
|
+
SHARE_FLOOR_SECONDS = 6 * ENGINE_START_SECONDS;
|
|
18707
18827
|
});
|
|
18708
18828
|
|
|
18709
18829
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
@@ -48345,7 +48465,7 @@ function totalsSince(now, mark) {
|
|
|
48345
48465
|
}
|
|
48346
48466
|
}
|
|
48347
48467
|
shrunk += Object.keys(mark.functions).filter((signature) => now.functions[signature] === undefined && (mark.functions[signature]?.calls ?? 0) > 0).length;
|
|
48348
|
-
const { dropped: droppedInAll, ...kept } = now;
|
|
48468
|
+
const { dropped: droppedInAll, firstFrame: _first, lastFrame: _last, ...kept } = now;
|
|
48349
48469
|
const dropped = (droppedInAll ?? 0) - (mark.dropped ?? 0);
|
|
48350
48470
|
return {
|
|
48351
48471
|
totals: {
|
|
@@ -49830,6 +49950,15 @@ function profileAnswer(reading, limit) {
|
|
|
49830
49950
|
} else {
|
|
49831
49951
|
notes.push(`These are the ${totals.frames === 1 ? "one frame" : `${totals.frames} frames`} that arrived, summed, without the engine's own totals, which it sends only when the profiler is switched off.${totalsWithheld} Not in them is any script time after the last frame sent: a frame still queued as the game exited, since the engine stops sending without emptying its queue, and what scripts did as it quit, such as in _exit_tree. That can be nothing: a scene that does its work in _ready and quits has all of it in its first frame.`);
|
|
49832
49952
|
}
|
|
49953
|
+
const unprofiled = totals.firstFrame ?? 0;
|
|
49954
|
+
if (reading.through === "gdharness" && unprofiled > 0) {
|
|
49955
|
+
notes.push(`The profiler came on at the game's frame ${unprofiled}, so ${unprofiled === 1 ? "its first frame is" : `its first ${unprofiled} frames are`} not in these, the main scene's _ready among them${reading.runtimeAddon === true ? "" : ": without the runtime addon nothing holds the game's start until the profiler is on"}.`);
|
|
49956
|
+
}
|
|
49957
|
+
const unarrived = totals.firstFrame === undefined || totals.lastFrame === undefined ? 0 : totals.lastFrame - totals.firstFrame + 1 - totals.frames - totals.unreadable;
|
|
49958
|
+
if (unarrived > 0) {
|
|
49959
|
+
const howMany = unarrived === 1 ? "One" : String(unarrived);
|
|
49960
|
+
notes.push(`${howMany} of the game's frames between the first and the last that arrived did not arrive, so ${unarrived === 1 ? "its" : "their"} script time is not in these.`);
|
|
49961
|
+
}
|
|
49833
49962
|
if (table.superCallers > 0) {
|
|
49834
49963
|
notes.push(`${table.superCallers === 1 ? "One function calls" : `${table.superCallers} functions call`} through super, marked selfIncludesSuper: the engine takes every other call out of the caller's self time, and not a super call, so ${table.superCallers === 1 ? "its" : "their"} selfMs holds the overridden function's time, which that function's own row counts again. Self time therefore cannot be added along an override chain, and scriptMs counts the time under a super call once more for each level above it.`);
|
|
49835
49964
|
}
|
|
@@ -49853,6 +49982,8 @@ function profileAnswer(reading, limit) {
|
|
|
49853
49982
|
connected: true,
|
|
49854
49983
|
complete: totals.complete,
|
|
49855
49984
|
frames: totals.frames,
|
|
49985
|
+
firstFrame: totals.firstFrame,
|
|
49986
|
+
lastFrame: totals.lastFrame,
|
|
49856
49987
|
coveredMs: reading.coveredMs,
|
|
49857
49988
|
scriptMs: table.scriptMs,
|
|
49858
49989
|
perCallOverheadUs: table.perCallUs ?? undefined,
|
|
@@ -53601,8 +53732,21 @@ ${error.text}`;
|
|
|
53601
53732
|
"editor_launch restart without hidden restarts it as it is"
|
|
53602
53733
|
]);
|
|
53603
53734
|
}
|
|
53735
|
+
let hidden = args["hidden"] === true;
|
|
53736
|
+
let silent = resolveSilent(args["silent"]);
|
|
53737
|
+
if (mine && (typeof args["hidden"] !== "boolean" || typeof args["silent"] !== "boolean")) {
|
|
53738
|
+
const was = before.editorPid === undefined ? null : await runningAs(before.editorPid);
|
|
53739
|
+
if (was?.kind === "commandLine") {
|
|
53740
|
+
const as = startedAs(was.text);
|
|
53741
|
+
hidden = typeof args["hidden"] === "boolean" ? args["hidden"] : as.hidden;
|
|
53742
|
+
silent = typeof args["silent"] === "boolean" ? args["silent"] : as.silent;
|
|
53743
|
+
} else if (typeof args["hidden"] !== "boolean") {
|
|
53744
|
+
const why = was === null ? "the operating system would not say" : was.unread ?? "it gave no command line";
|
|
53745
|
+
return this.createErrorResponse(`How the editor (pid ${before.editorPid ?? "unknown"}) was started could not be read (${why}), so whether it was hidden cannot be told and it was left running.`, ["Pass hidden: true to bring it back with no window, or hidden: false to bring it back visible"]);
|
|
53746
|
+
}
|
|
53747
|
+
}
|
|
53604
53748
|
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
53605
|
-
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid,
|
|
53749
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, hidden, silent) : await this.handleViaBridge("restart_editor", {});
|
|
53606
53750
|
if (asked.isError === true) {
|
|
53607
53751
|
return asked;
|
|
53608
53752
|
}
|
|
@@ -53621,6 +53765,7 @@ ${error.text}`;
|
|
|
53621
53765
|
const after = this.settingKeysOf(before.projectPath);
|
|
53622
53766
|
return this.jsonTextResponse({
|
|
53623
53767
|
restarted: true,
|
|
53768
|
+
hidden: mine && hidden,
|
|
53624
53769
|
editorPid: now.editorPid,
|
|
53625
53770
|
addonVersion: now.addonVersion,
|
|
53626
53771
|
serverVersion: SERVER_VERSION,
|
|
@@ -58408,7 +58553,7 @@ var VALUED2 = new Set([
|
|
|
58408
58553
|
"--order",
|
|
58409
58554
|
"--result"
|
|
58410
58555
|
]);
|
|
58411
|
-
var SWITCHES = new Set(["--changed-only", "--import", "--print-output"]);
|
|
58556
|
+
var SWITCHES = new Set(["--changed-only", "--import", "--print-output", "--verbose"]);
|
|
58412
58557
|
function testOptionsOf(args, cwd) {
|
|
58413
58558
|
const valued = new Map;
|
|
58414
58559
|
const switches = new Set;
|
|
@@ -58456,6 +58601,7 @@ function testOptionsOf(args, cwd) {
|
|
|
58456
58601
|
importFirst: switches.has("--import"),
|
|
58457
58602
|
order: order === undefined ? null : order === "reversed" ? "reversed" : seedOf(order),
|
|
58458
58603
|
printOutput: switches.has("--print-output"),
|
|
58604
|
+
verbose: switches.has("--verbose"),
|
|
58459
58605
|
resultFile: last("--result") === undefined ? null : resolve10(cwd, last("--result") ?? "")
|
|
58460
58606
|
};
|
|
58461
58607
|
}
|
|
@@ -58615,7 +58761,8 @@ async function runTestCommand(options, godotPath) {
|
|
|
58615
58761
|
running,
|
|
58616
58762
|
selfTyped: selfTypedExports(scripts.sources),
|
|
58617
58763
|
godotVersion: options.godotVersion,
|
|
58618
|
-
cores: options.cores
|
|
58764
|
+
cores: options.cores,
|
|
58765
|
+
verbose: options.verbose
|
|
58619
58766
|
};
|
|
58620
58767
|
try {
|
|
58621
58768
|
if (options.importFirst) {
|
|
@@ -58687,7 +58834,19 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58687
58834
|
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(byRows)}.`);
|
|
58688
58835
|
}
|
|
58689
58836
|
const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
|
|
58690
|
-
|
|
58837
|
+
const taken = [];
|
|
58838
|
+
while (taken.length < lanes.length) {
|
|
58839
|
+
const lock = await slots.takeFree();
|
|
58840
|
+
if (lock === null) {
|
|
58841
|
+
break;
|
|
58842
|
+
}
|
|
58843
|
+
taken.push(lock);
|
|
58844
|
+
}
|
|
58845
|
+
const shared = options.order === null && lanes.length > 1 && taken.length < lanes.length;
|
|
58846
|
+
if (shared) {
|
|
58847
|
+
console.log(sharesSaid(taken.length, lanes.length));
|
|
58848
|
+
}
|
|
58849
|
+
let ran = shared ? await runShares(launch, units, declared, lanes.length, taken, times) : (await Promise.all(lanes.map((lane, at) => runLane(launch, lane, times, taken[at] ?? null)))).flat().sort((a, b) => a.index - b.index);
|
|
58691
58850
|
if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
|
|
58692
58851
|
console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
|
|
58693
58852
|
const again = [];
|
|
@@ -58705,6 +58864,9 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58705
58864
|
process14.off("SIGTERM", interrupted);
|
|
58706
58865
|
}
|
|
58707
58866
|
}
|
|
58867
|
+
function sharesSaid(free, lanes) {
|
|
58868
|
+
return `${free === 0 ? "None" : String(free)} of the ${String(lanes)} slots this run uses ${free === 1 ? "was" : "were"} free as it began, so its engines are handed the suites a share at a time as each starts, and an engine starts as each slot comes free.`;
|
|
58869
|
+
}
|
|
58708
58870
|
function roomSaid(engines, lanes, unheld, held, asked) {
|
|
58709
58871
|
if (held === undefined || lanes >= unheld) {
|
|
58710
58872
|
return "";
|
|
@@ -58783,8 +58945,8 @@ async function importProject(launch) {
|
|
|
58783
58945
|
launch.slots.release(lock);
|
|
58784
58946
|
}
|
|
58785
58947
|
}
|
|
58786
|
-
async function runLane(launch, lane, times) {
|
|
58787
|
-
const lock = await launch.slots.take();
|
|
58948
|
+
async function runLane(launch, lane, times, taken) {
|
|
58949
|
+
const lock = taken ?? await launch.slots.take();
|
|
58788
58950
|
try {
|
|
58789
58951
|
const ran = [];
|
|
58790
58952
|
for (const { plan, index } of lane) {
|
|
@@ -58795,6 +58957,39 @@ async function runLane(launch, lane, times) {
|
|
|
58795
58957
|
launch.slots.release(lock);
|
|
58796
58958
|
}
|
|
58797
58959
|
}
|
|
58960
|
+
async function runShares(launch, units, declared, lanes, taken, times) {
|
|
58961
|
+
const shares = new Shares(units, declared, lanes);
|
|
58962
|
+
const handedOut = new AbortController;
|
|
58963
|
+
let engines = 0;
|
|
58964
|
+
const lane = async (at) => {
|
|
58965
|
+
let lock = taken[at] ?? null;
|
|
58966
|
+
if (lock === null) {
|
|
58967
|
+
try {
|
|
58968
|
+
lock = await launch.slots.take(handedOut.signal);
|
|
58969
|
+
} catch (error) {
|
|
58970
|
+
if (handedOut.signal.aborted) {
|
|
58971
|
+
return [];
|
|
58972
|
+
}
|
|
58973
|
+
throw error;
|
|
58974
|
+
}
|
|
58975
|
+
}
|
|
58976
|
+
try {
|
|
58977
|
+
const ran = [];
|
|
58978
|
+
for (let plan = shares.next(at);plan !== null; plan = shares.next(at)) {
|
|
58979
|
+
if (shares.empty) {
|
|
58980
|
+
handedOut.abort();
|
|
58981
|
+
}
|
|
58982
|
+
const index = engines;
|
|
58983
|
+
engines += 1;
|
|
58984
|
+
ran.push(await runEngine2(launch, plan, index, times, "", lock));
|
|
58985
|
+
}
|
|
58986
|
+
return ran;
|
|
58987
|
+
} finally {
|
|
58988
|
+
launch.slots.release(lock);
|
|
58989
|
+
}
|
|
58990
|
+
};
|
|
58991
|
+
return (await Promise.all(Array.from({ length: lanes }, (_, at) => lane(at)))).flat().sort((a, b) => a.index - b.index);
|
|
58992
|
+
}
|
|
58798
58993
|
async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
58799
58994
|
const directory = join27(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
|
|
58800
58995
|
mkdirSync10(join27(directory, "user"), { recursive: true });
|
|
@@ -58822,6 +59017,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
|
58822
59017
|
"Dummy",
|
|
58823
59018
|
"--path",
|
|
58824
59019
|
launch.projectPath,
|
|
59020
|
+
...launch.verbose ? ["--verbose"] : [],
|
|
58825
59021
|
"-s",
|
|
58826
59022
|
shippedListedRunner(),
|
|
58827
59023
|
...plan.windowed ? [] : ["--ignoreHeadlessMode"],
|
|
@@ -59474,7 +59670,7 @@ Usage:
|
|
|
59474
59670
|
gdharness test [project] [suite ...] [--engines N] [--slots DIR] [--slot-count N]
|
|
59475
59671
|
[--silence S] [--deadline S] [--tests DIR] [--changed-only] [--reach-also PATH]
|
|
59476
59672
|
[--import] [--godot PATH] [--godot-version V] [--cores N]
|
|
59477
|
-
[--order reversed|SEED] [--print-output] [--result FILE] [--runner]
|
|
59673
|
+
[--order reversed|SEED] [--print-output] [--verbose] [--result FILE] [--runner]
|
|
59478
59674
|
Run the project's gdUnit4 suites over several engines and
|
|
59479
59675
|
judge every line they print: exit 1 on a failing case, an
|
|
59480
59676
|
engine error no case declared, a case that never finished
|
|
@@ -59483,7 +59679,8 @@ Usage:
|
|
|
59483
59679
|
suites that passed with everything they reach unchanged; it
|
|
59484
59680
|
is for iterating, and no gate passes it. --print-output
|
|
59485
59681
|
prints what every engine printed, for a
|
|
59486
|
-
benchmark run by hand. --
|
|
59682
|
+
benchmark run by hand. --verbose starts every engine verbose,
|
|
59683
|
+
so a leak at exit is named. --result writes the outcome, each
|
|
59487
59684
|
suite's time and every problem as JSON for a gate. --runner
|
|
59488
59685
|
prints the shipped runner's path. docs/test-runs.md has the
|
|
59489
59686
|
rest.
|
|
@@ -133,6 +133,21 @@ func _reload_script(path: String) -> void:
|
|
|
133
133
|
|
|
134
134
|
func _reload_scene(path: String) -> void:
|
|
135
135
|
var edited_scene: Node = EditorInterface.get_edited_scene_root()
|
|
136
|
-
if edited_scene
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
if not edited_scene or edited_scene.scene_file_path != path:
|
|
137
|
+
return
|
|
138
|
+
# A reload from the file drops the edits the editor holds, without a prompt. gdharness refuses to
|
|
139
|
+
# write over them, so what is seen here is a write by somebody else or one the editor's tab has
|
|
140
|
+
# already taken in, made just before an edit inside the same poll, and neither is worth the edit.
|
|
141
|
+
if EditorInterface.get_unsaved_scenes().has(path):
|
|
142
|
+
push_warning(
|
|
143
|
+
(
|
|
144
|
+
(
|
|
145
|
+
"gdharness: %s changed on disk while the editor holds unsaved changes to it, so it "
|
|
146
|
+
+ "was not reloaded; saving it writes the editor's copy over the file"
|
|
147
|
+
)
|
|
148
|
+
% path
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
|
+
print("[gdharness] Scene changed on disk, reloading: ", path)
|
|
153
|
+
EditorInterface.reload_scene_from_path(path)
|
|
@@ -20,21 +20,27 @@ extends RefCounted
|
|
|
20
20
|
## A label is asked about first and read directly, because a screen is mostly labels and this runs
|
|
21
21
|
## for each of them on every look a wait takes.
|
|
22
22
|
static func said_by(node: Node) -> String:
|
|
23
|
-
var
|
|
24
|
-
if label != null:
|
|
25
|
-
return _label_says(label)
|
|
26
|
-
if draws_a_list(node):
|
|
27
|
-
return "\n".join(lines_said_by(node))
|
|
28
|
-
var own: String = _text_of(node)
|
|
23
|
+
var own: String = drawn_by(node)
|
|
29
24
|
# A control showing a picture rather than words is named for a player who cannot see it, and that
|
|
30
25
|
# name is the one the words a caller asks by are meant for: the rail's "Out" tab is a square with
|
|
31
26
|
# an icon on it, and a click on "Out" found only the words around it.
|
|
32
|
-
if own.is_empty() and node is Control:
|
|
27
|
+
if own.is_empty() and node is Control and not node is Label and not draws_a_list(node):
|
|
33
28
|
var control: Control = node
|
|
34
29
|
return control.atr(control.accessibility_name).strip_edges()
|
|
35
30
|
return own
|
|
36
31
|
|
|
37
32
|
|
|
33
|
+
## The words [param node] draws: what [method said_by] answers, without the name a control is given
|
|
34
|
+
## for a player who cannot see it, which nothing on the screen shows.
|
|
35
|
+
static func drawn_by(node: Node) -> String:
|
|
36
|
+
var label: Label = node as Label
|
|
37
|
+
if label != null:
|
|
38
|
+
return _label_says(label)
|
|
39
|
+
if draws_a_list(node):
|
|
40
|
+
return "\n".join(lines_said_by(node))
|
|
41
|
+
return _text_of(node)
|
|
42
|
+
|
|
43
|
+
|
|
38
44
|
## Every line [param node] draws for a player to read, one to an entry: a label's one line, or each
|
|
39
45
|
## item of a control that draws a list of them. Tab titles, list items, tree rows, menu titles and
|
|
40
46
|
## the items of an open menu are held by the control rather than in any `text`, so a screen read as
|
|
@@ -55,10 +61,12 @@ static func lines_said_by(node: Node) -> Array[String]:
|
|
|
55
61
|
return lines
|
|
56
62
|
|
|
57
63
|
|
|
58
|
-
## The node [param node] is drawn as a copy of, or null: the nearest node above it that
|
|
59
|
-
##
|
|
64
|
+
## The node [param node] is drawn as a copy of, or null: the nearest node above it that draws any
|
|
65
|
+
## words, when it draws the same ones, followed up while that one is a copy in turn. A word drawn
|
|
60
66
|
## with an outline or a glow is often a label holding copies of itself, offset or tinted, and a
|
|
61
|
-
## player reads one sign where a find counted four.
|
|
67
|
+
## player reads one sign where a find counted four. Drawn words only: a page's root named for a
|
|
68
|
+
## screen reader by the title its heading shows draws nothing, and taken for the original it left
|
|
69
|
+
## the heading read as a copy of words nobody reads.
|
|
62
70
|
static func copied_from(node: Node) -> Node:
|
|
63
71
|
var chain: Array[Node] = copied_through(node)
|
|
64
72
|
return null if chain.is_empty() else chain.back()
|
|
@@ -68,12 +76,12 @@ static func copied_from(node: Node) -> Node:
|
|
|
68
76
|
## [method copied_from], which is the last of them.
|
|
69
77
|
static func copied_through(node: Node) -> Array[Node]:
|
|
70
78
|
var chain: Array[Node] = []
|
|
71
|
-
var words: String =
|
|
79
|
+
var words: String = drawn_by(node)
|
|
72
80
|
if words.is_empty():
|
|
73
81
|
return chain
|
|
74
82
|
var above: Node = node.get_parent()
|
|
75
83
|
while above != null:
|
|
76
|
-
var said: String =
|
|
84
|
+
var said: String = drawn_by(above)
|
|
77
85
|
if not said.is_empty():
|
|
78
86
|
if said != words:
|
|
79
87
|
break
|
package/build/keeper.js
CHANGED
|
@@ -9795,6 +9795,8 @@ class ProfileAggregate {
|
|
|
9795
9795
|
complete = false;
|
|
9796
9796
|
frames = 0;
|
|
9797
9797
|
lastFrameAt = null;
|
|
9798
|
+
firstFrame = null;
|
|
9799
|
+
lastFrame = null;
|
|
9798
9800
|
unreadable = 0;
|
|
9799
9801
|
cappedFrames = 0;
|
|
9800
9802
|
totalCapped = false;
|
|
@@ -9826,6 +9828,11 @@ class ProfileAggregate {
|
|
|
9826
9828
|
this.frames += 1;
|
|
9827
9829
|
this.lastFrameAt = now;
|
|
9828
9830
|
this.cappedFrames += carried >= this.frameFunctions ? 1 : 0;
|
|
9831
|
+
const numbered = received.data[0];
|
|
9832
|
+
if (typeof numbered === "number") {
|
|
9833
|
+
this.firstFrame ??= numbered;
|
|
9834
|
+
this.lastFrame = numbered;
|
|
9835
|
+
}
|
|
9829
9836
|
}
|
|
9830
9837
|
} else if (received.message === "servers:profile_total") {
|
|
9831
9838
|
const whole = new Map;
|
|
@@ -9884,7 +9891,8 @@ class ProfileAggregate {
|
|
|
9884
9891
|
frameFunctions: this.frameFunctions,
|
|
9885
9892
|
cappedFrames: this.cappedFrames,
|
|
9886
9893
|
totalCapped: this.totalCapped,
|
|
9887
|
-
...this.dropped > 0 ? { dropped: this.dropped } : {}
|
|
9894
|
+
...this.dropped > 0 ? { dropped: this.dropped } : {},
|
|
9895
|
+
...this.firstFrame === null || this.lastFrame === null ? {} : { firstFrame: this.firstFrame, lastFrame: this.lastFrame }
|
|
9888
9896
|
};
|
|
9889
9897
|
}
|
|
9890
9898
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.30",
|
|
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",
|