gdharness 1.2.0 → 1.2.2
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 +58 -18
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +87 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +6 -1
- package/build/index.js +58 -18
- package/build/keeper.js +4 -2
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17977,7 +17977,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17977
17977
|
},
|
|
17978
17978
|
{
|
|
17979
17979
|
name: "project_test",
|
|
17980
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17980
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, every report under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17981
17981
|
parameters: {
|
|
17982
17982
|
projectPath: PROJECT_PATH,
|
|
17983
17983
|
path: {
|
|
@@ -18421,7 +18421,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18421
18421
|
},
|
|
18422
18422
|
{
|
|
18423
18423
|
name: "editor_output",
|
|
18424
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
18424
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
18425
18425
|
parameters: {
|
|
18426
18426
|
severity: {
|
|
18427
18427
|
type: "string",
|
|
@@ -18469,7 +18469,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18469
18469
|
},
|
|
18470
18470
|
{
|
|
18471
18471
|
name: "editor_status",
|
|
18472
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
18472
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
18473
18473
|
parameters: {},
|
|
18474
18474
|
requires: []
|
|
18475
18475
|
},
|
|
@@ -39250,18 +39250,25 @@ function count(attributes, name) {
|
|
|
39250
39250
|
return Number.isFinite(value) ? value : 0;
|
|
39251
39251
|
}
|
|
39252
39252
|
function caseOf(suite, element) {
|
|
39253
|
-
const
|
|
39254
|
-
|
|
39255
|
-
|
|
39253
|
+
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
39254
|
+
const detail = child.text.trim();
|
|
39255
|
+
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
39256
|
+
});
|
|
39257
|
+
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
39258
|
+
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
39256
39259
|
return {
|
|
39257
39260
|
suite: suite.attributes["name"] ?? "",
|
|
39258
39261
|
name: element.attributes["name"] ?? "",
|
|
39259
39262
|
status,
|
|
39260
39263
|
time: count(element.attributes, "time"),
|
|
39261
|
-
message:
|
|
39262
|
-
detail: detail
|
|
39264
|
+
message: reports[0]?.message ?? null,
|
|
39265
|
+
detail: reports[0]?.detail ?? null,
|
|
39266
|
+
reports
|
|
39263
39267
|
};
|
|
39264
39268
|
}
|
|
39269
|
+
function failedAnAssertion(entry) {
|
|
39270
|
+
return entry.reports.some((report) => report.kind === "failure");
|
|
39271
|
+
}
|
|
39265
39272
|
function pathOf(suite) {
|
|
39266
39273
|
const directory = suite.attributes["package"];
|
|
39267
39274
|
const name = suite.attributes["name"];
|
|
@@ -39276,7 +39283,7 @@ function parseJUnit(xml) {
|
|
|
39276
39283
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
39277
39284
|
const suites = suiteElements.map((suite) => {
|
|
39278
39285
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
39279
|
-
const failures = cases.filter(
|
|
39286
|
+
const failures = cases.filter(failedAnAssertion).length;
|
|
39280
39287
|
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
39281
39288
|
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
39282
39289
|
return {
|
|
@@ -39296,7 +39303,7 @@ function parseJUnit(xml) {
|
|
|
39296
39303
|
const all = suites.flatMap((suite) => suite.cases);
|
|
39297
39304
|
return {
|
|
39298
39305
|
tests: all.length,
|
|
39299
|
-
failures: all.filter(
|
|
39306
|
+
failures: all.filter(failedAnAssertion).length,
|
|
39300
39307
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
39301
39308
|
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
39302
39309
|
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
@@ -39418,6 +39425,25 @@ ${note}`;
|
|
|
39418
39425
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
39419
39426
|
});
|
|
39420
39427
|
}
|
|
39428
|
+
function failedCases(report, printed) {
|
|
39429
|
+
const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
|
|
39430
|
+
const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
|
|
39431
|
+
let next = 0;
|
|
39432
|
+
return cases.map(({ entry, path }) => {
|
|
39433
|
+
const reports = read.slice(next, next + entry.reports.length);
|
|
39434
|
+
next += entry.reports.length;
|
|
39435
|
+
return {
|
|
39436
|
+
suite: entry.suite,
|
|
39437
|
+
name: entry.name,
|
|
39438
|
+
status: entry.status,
|
|
39439
|
+
time: entry.time,
|
|
39440
|
+
path,
|
|
39441
|
+
message: reports[0]?.message ?? null,
|
|
39442
|
+
detail: reports[0]?.detail ?? null,
|
|
39443
|
+
...reports.length > 1 ? { reports } : {}
|
|
39444
|
+
};
|
|
39445
|
+
});
|
|
39446
|
+
}
|
|
39421
39447
|
function masked(expected, actual) {
|
|
39422
39448
|
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
39423
39449
|
return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
|
|
@@ -42202,7 +42228,8 @@ function recordAt(path) {
|
|
|
42202
42228
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
42203
42229
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
42204
42230
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
42205
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
42231
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
42232
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
42206
42233
|
};
|
|
42207
42234
|
}
|
|
42208
42235
|
async function runningAs(pid) {
|
|
@@ -43482,7 +43509,7 @@ function profileAnswer(reading, limit) {
|
|
|
43482
43509
|
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.`);
|
|
43483
43510
|
}
|
|
43484
43511
|
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
43485
|
-
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs:
|
|
43512
|
+
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: that much of its time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
|
|
43486
43513
|
}
|
|
43487
43514
|
notes.push(...cappedNotes(totals));
|
|
43488
43515
|
if (totals.unreadable > 0) {
|
|
@@ -45431,7 +45458,7 @@ class GodotServer {
|
|
|
45431
45458
|
isError: true
|
|
45432
45459
|
};
|
|
45433
45460
|
}
|
|
45434
|
-
const failed =
|
|
45461
|
+
const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
|
|
45435
45462
|
const orphans = orphansPrinted(said);
|
|
45436
45463
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
45437
45464
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -45932,7 +45959,9 @@ class GodotServer {
|
|
|
45932
45959
|
return {
|
|
45933
45960
|
pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
|
|
45934
45961
|
...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
|
|
45935
|
-
...run.pickedUp === true ? { adopted: true } : {}
|
|
45962
|
+
...run.pickedUp === true ? { adopted: true } : {},
|
|
45963
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
45964
|
+
...run.endedAt === undefined ? {} : { endedAt: new Date(run.endedAt).toISOString() }
|
|
45936
45965
|
};
|
|
45937
45966
|
}
|
|
45938
45967
|
async handleEditorStatus() {
|
|
@@ -46705,10 +46734,11 @@ class GodotServer {
|
|
|
46705
46734
|
return "unreached";
|
|
46706
46735
|
}
|
|
46707
46736
|
running.endedThrough = "editor";
|
|
46708
|
-
|
|
46709
|
-
|
|
46737
|
+
const stopped = announced === undefined ? await this.untilTheEditorStopsPlaying() : await untilGone([announced]);
|
|
46738
|
+
if (stopped) {
|
|
46739
|
+
running.endedAt = Date.now();
|
|
46710
46740
|
}
|
|
46711
|
-
return
|
|
46741
|
+
return stopped ? "gone" : "lingering";
|
|
46712
46742
|
}
|
|
46713
46743
|
if (running.pid === null) {
|
|
46714
46744
|
return "gone";
|
|
@@ -46757,6 +46787,7 @@ class GodotServer {
|
|
|
46757
46787
|
if (landed && gone) {
|
|
46758
46788
|
running.exitCode = null;
|
|
46759
46789
|
running.exitSignal = "SIGTERM";
|
|
46790
|
+
running.endedAt ??= Date.now();
|
|
46760
46791
|
}
|
|
46761
46792
|
if (landed) {
|
|
46762
46793
|
running.endedThrough = "signal";
|
|
@@ -46781,6 +46812,7 @@ class GodotServer {
|
|
|
46781
46812
|
if (asked === "signalled" && gone) {
|
|
46782
46813
|
run.exitCode = null;
|
|
46783
46814
|
run.exitSignal = "SIGTERM";
|
|
46815
|
+
run.endedAt ??= Date.now();
|
|
46784
46816
|
}
|
|
46785
46817
|
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
|
|
46786
46818
|
return gone ? "gone" : "lingering";
|
|
@@ -46828,6 +46860,7 @@ class GodotServer {
|
|
|
46828
46860
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
46829
46861
|
started.exitCode = code;
|
|
46830
46862
|
started.exitSignal = code === null ? signal : null;
|
|
46863
|
+
started.endedAt = Date.now();
|
|
46831
46864
|
});
|
|
46832
46865
|
child.on("error", (err) => {
|
|
46833
46866
|
console.error("Failed to start Godot process:", err);
|
|
@@ -47142,6 +47175,9 @@ class GodotServer {
|
|
|
47142
47175
|
adopted.log.finish();
|
|
47143
47176
|
adopted.exitCode = record.exitCode ?? null;
|
|
47144
47177
|
adopted.exitSignal = record.exitSignal ?? null;
|
|
47178
|
+
if (record.endedAt !== undefined) {
|
|
47179
|
+
adopted.endedAt = record.endedAt;
|
|
47180
|
+
}
|
|
47145
47181
|
const exitMayBeComing = !alive(record.pid) && !exited(adopted);
|
|
47146
47182
|
if (!exitMayBeComing) {
|
|
47147
47183
|
adopted.endedUnwatched = !exited(adopted);
|
|
@@ -47435,6 +47471,9 @@ class GodotServer {
|
|
|
47435
47471
|
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
47436
47472
|
run.exitCode = record.exitCode ?? null;
|
|
47437
47473
|
run.exitSignal = record.exitSignal ?? null;
|
|
47474
|
+
if (record.endedAt !== undefined) {
|
|
47475
|
+
run.endedAt = record.endedAt;
|
|
47476
|
+
}
|
|
47438
47477
|
return true;
|
|
47439
47478
|
}
|
|
47440
47479
|
if (Date.now() >= until) {
|
|
@@ -47581,7 +47620,8 @@ class GodotServer {
|
|
|
47581
47620
|
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
47582
47621
|
transcript: run.transcript ?? undefined,
|
|
47583
47622
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
47584
|
-
|
|
47623
|
+
endedAt: stillRunning(run) || run.endedAt === undefined ? undefined : new Date(run.endedAt).toISOString(),
|
|
47624
|
+
elapsedMs: stillRunning(run) ? Date.now() - run.startedAt : run.endedAt === undefined ? undefined : run.endedAt - run.startedAt,
|
|
47585
47625
|
cpuSeconds,
|
|
47586
47626
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
47587
47627
|
omitted: selected.omitted,
|
|
@@ -201,8 +201,8 @@ func _send_the_profile() -> void:
|
|
|
201
201
|
## What the script profiler costs per call in this game, measured once each time it is switched on.
|
|
202
202
|
##
|
|
203
203
|
## The profiler times every call it counts, and that bookkeeping lands in the self time of the
|
|
204
|
-
## function called
|
|
205
|
-
## second of its own
|
|
204
|
+
## function called, so a function doing almost nothing, called six hundred thousand times, can read
|
|
205
|
+
## as a second of its own that is mostly the profiler's. So an empty function
|
|
206
206
|
## is called here [constant PROFILER_PROBE_CALLS] times while the profiler watches, under the load
|
|
207
207
|
## the game is under, and the server reads its row as the cost of one call.
|
|
208
208
|
func _calibrate_the_profiler() -> void:
|
|
@@ -560,7 +560,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
560
560
|
## that does not process while the game is paused is hovered and never handed the event.
|
|
561
561
|
func _takes_no_click(control: Control, node_path: String) -> String:
|
|
562
562
|
if not control.is_visible_in_tree():
|
|
563
|
-
return "%s is not visible, so nothing can click it" % node_path
|
|
563
|
+
return "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(control)]
|
|
564
564
|
var button: BaseButton = control as BaseButton
|
|
565
565
|
if button != null and button.disabled:
|
|
566
566
|
return "%s is disabled, so clicking it presses nothing" % node_path
|
|
@@ -681,7 +681,10 @@ func _press_button(viewport: Viewport, event: InputEventMouseButton) -> void:
|
|
|
681
681
|
## anything the engine can be asked, so it is not claimed.
|
|
682
682
|
func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) -> Dictionary:
|
|
683
683
|
if not item.is_visible_in_tree():
|
|
684
|
-
return {
|
|
684
|
+
return {
|
|
685
|
+
"type": "error",
|
|
686
|
+
"message": "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(item)]
|
|
687
|
+
}
|
|
685
688
|
|
|
686
689
|
var found: Dictionary = Queries.in_frame(item)
|
|
687
690
|
if found.is_empty():
|
|
@@ -376,6 +376,93 @@ static func drawn_over(target: Control) -> Node:
|
|
|
376
376
|
return _cover_of(target, drawn, _areas_of(drawn))
|
|
377
377
|
|
|
378
378
|
|
|
379
|
+
## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
|
|
380
|
+
## which nodes hide it, nearest first, and what is shown beside the nearest. Reading visible off
|
|
381
|
+
## the node itself says true when only an ancestor is hidden, so without this a caller had nothing
|
|
382
|
+
## to unhide; every hidden one is named because unhiding only the nearest leaves the rest hiding it.
|
|
383
|
+
## "" when nothing on the way up says so.
|
|
384
|
+
static func why_hidden(node: Node) -> String:
|
|
385
|
+
var hiding: Array[Node] = _hiding(node)
|
|
386
|
+
if hiding.is_empty():
|
|
387
|
+
return ""
|
|
388
|
+
var named: Array[String] = []
|
|
389
|
+
for one: Node in hiding:
|
|
390
|
+
if one == node:
|
|
391
|
+
named.append("its own visible is false")
|
|
392
|
+
else:
|
|
393
|
+
named.append("%s is hidden%s" % [one.get_path(), "" if named.is_empty() else " too"])
|
|
394
|
+
var beside: Array[String] = []
|
|
395
|
+
var parent: Node = hiding[0].get_parent()
|
|
396
|
+
if parent != null:
|
|
397
|
+
for sibling: Node in parent.get_children():
|
|
398
|
+
if sibling != hiding[0] and _shown(sibling):
|
|
399
|
+
beside.append(str(sibling.get_path()))
|
|
400
|
+
return " (%s%s)" % [", and ".join(named), _shown_beside(beside)]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
## "; A, B and C beside it are shown" for [param beside], naming three at most and counting the rest.
|
|
404
|
+
static func _shown_beside(beside: Array[String]) -> String:
|
|
405
|
+
if beside.is_empty():
|
|
406
|
+
return ""
|
|
407
|
+
if beside.size() == 1:
|
|
408
|
+
return "; %s beside it is shown" % beside[0]
|
|
409
|
+
var listed: Array[String] = beside.slice(0, 3)
|
|
410
|
+
var rest: int = beside.size() - listed.size()
|
|
411
|
+
if rest > 0:
|
|
412
|
+
return "; %s and %d more beside it are shown" % [", ".join(listed), rest]
|
|
413
|
+
var last: String = listed.pop_back()
|
|
414
|
+
return "; %s and %s beside it are shown" % [", ".join(listed), last]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
|
|
418
|
+
## the way the engine inherits visibility rather than by parent: an item under a plain Node is drawn
|
|
419
|
+
## from its canvas layer, whatever a control above that Node says, so naming that control would send
|
|
420
|
+
## a caller to unhide something that hides nothing.
|
|
421
|
+
static func _hiding(node: Node) -> Array[Node]:
|
|
422
|
+
var hiding: Array[Node] = []
|
|
423
|
+
var at: Node = node
|
|
424
|
+
while at != null:
|
|
425
|
+
var item: CanvasItem = at as CanvasItem
|
|
426
|
+
var spatial: Node3D = at as Node3D
|
|
427
|
+
if item != null:
|
|
428
|
+
if not item.visible:
|
|
429
|
+
hiding.append(item)
|
|
430
|
+
var above: CanvasItem = item.get_parent() as CanvasItem
|
|
431
|
+
if above != null:
|
|
432
|
+
at = above
|
|
433
|
+
continue
|
|
434
|
+
var layer: CanvasLayer = item.get_canvas_layer_node()
|
|
435
|
+
if layer != null and not layer.visible:
|
|
436
|
+
hiding.append(layer)
|
|
437
|
+
at = item.get_viewport()
|
|
438
|
+
elif spatial != null:
|
|
439
|
+
if not spatial.visible:
|
|
440
|
+
hiding.append(spatial)
|
|
441
|
+
at = spatial.get_parent_node_3d()
|
|
442
|
+
if at == null:
|
|
443
|
+
at = spatial.get_viewport()
|
|
444
|
+
else:
|
|
445
|
+
var window: Window = at as Window
|
|
446
|
+
if window != null and not window.visible:
|
|
447
|
+
hiding.append(window)
|
|
448
|
+
at = null
|
|
449
|
+
return hiding
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
## Whether [param node] is something drawn and on show, for naming what stands beside a hidden one.
|
|
453
|
+
static func _shown(node: Node) -> bool:
|
|
454
|
+
var item: CanvasItem = node as CanvasItem
|
|
455
|
+
if item != null:
|
|
456
|
+
return item.is_visible_in_tree()
|
|
457
|
+
var spatial: Node3D = node as Node3D
|
|
458
|
+
if spatial != null:
|
|
459
|
+
return spatial.is_visible_in_tree()
|
|
460
|
+
var layer: CanvasLayer = node as CanvasLayer
|
|
461
|
+
if layer != null:
|
|
462
|
+
return layer.visible
|
|
463
|
+
return false
|
|
464
|
+
|
|
465
|
+
|
|
379
466
|
## Everything in the tree [param root] heads that a pointer can land on: each control on screen
|
|
380
467
|
## that does not let the pointer through, and each window embedded in another.
|
|
381
468
|
static func _pointer_takers(root: Node) -> Array[Node]:
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
|
|
6
6
|
const Menus = preload("runtime_menus.gd")
|
|
7
7
|
const Read = preload("reading.gd")
|
|
8
|
+
const Targets = preload("runtime_targets.gd")
|
|
8
9
|
const Values = preload("runtime_values.gd")
|
|
9
10
|
const Words = preload("runtime_words.gd")
|
|
10
11
|
|
|
@@ -251,7 +252,11 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
251
252
|
if holder != null and holder.disabled:
|
|
252
253
|
return {"type": "error", "message": "%s is disabled, so its menu cannot be opened" % node_path}
|
|
253
254
|
if holder != null and not holder.is_visible_in_tree():
|
|
254
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
"type": "error",
|
|
257
|
+
"message":
|
|
258
|
+
"%s is not visible%s, so its menu cannot be opened" % [node_path, Targets.why_hidden(holder)]
|
|
259
|
+
}
|
|
255
260
|
|
|
256
261
|
# Shown first, because a menu nobody has opened has no focus to move and the press would go to
|
|
257
262
|
# whatever is behind it, and before the item is looked for, because opening is when a game fills
|
package/build/index.js
CHANGED
|
@@ -29817,18 +29817,25 @@ function count(attributes, name) {
|
|
|
29817
29817
|
return Number.isFinite(value) ? value : 0;
|
|
29818
29818
|
}
|
|
29819
29819
|
function caseOf(suite, element) {
|
|
29820
|
-
const
|
|
29821
|
-
|
|
29822
|
-
|
|
29820
|
+
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
29821
|
+
const detail = child.text.trim();
|
|
29822
|
+
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
29823
|
+
});
|
|
29824
|
+
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
29825
|
+
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
29823
29826
|
return {
|
|
29824
29827
|
suite: suite.attributes["name"] ?? "",
|
|
29825
29828
|
name: element.attributes["name"] ?? "",
|
|
29826
29829
|
status,
|
|
29827
29830
|
time: count(element.attributes, "time"),
|
|
29828
|
-
message:
|
|
29829
|
-
detail: detail
|
|
29831
|
+
message: reports[0]?.message ?? null,
|
|
29832
|
+
detail: reports[0]?.detail ?? null,
|
|
29833
|
+
reports
|
|
29830
29834
|
};
|
|
29831
29835
|
}
|
|
29836
|
+
function failedAnAssertion(entry) {
|
|
29837
|
+
return entry.reports.some((report) => report.kind === "failure");
|
|
29838
|
+
}
|
|
29832
29839
|
function pathOf(suite) {
|
|
29833
29840
|
const directory = suite.attributes["package"];
|
|
29834
29841
|
const name = suite.attributes["name"];
|
|
@@ -29843,7 +29850,7 @@ function parseJUnit(xml) {
|
|
|
29843
29850
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
29844
29851
|
const suites = suiteElements.map((suite) => {
|
|
29845
29852
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
29846
|
-
const failures = cases.filter(
|
|
29853
|
+
const failures = cases.filter(failedAnAssertion).length;
|
|
29847
29854
|
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
29848
29855
|
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
29849
29856
|
return {
|
|
@@ -29863,7 +29870,7 @@ function parseJUnit(xml) {
|
|
|
29863
29870
|
const all = suites.flatMap((suite) => suite.cases);
|
|
29864
29871
|
return {
|
|
29865
29872
|
tests: all.length,
|
|
29866
|
-
failures: all.filter(
|
|
29873
|
+
failures: all.filter(failedAnAssertion).length,
|
|
29867
29874
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
29868
29875
|
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
29869
29876
|
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
@@ -30010,6 +30017,25 @@ ${note}`;
|
|
|
30010
30017
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
30011
30018
|
});
|
|
30012
30019
|
}
|
|
30020
|
+
function failedCases(report, printed) {
|
|
30021
|
+
const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
|
|
30022
|
+
const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
|
|
30023
|
+
let next = 0;
|
|
30024
|
+
return cases.map(({ entry, path }) => {
|
|
30025
|
+
const reports = read.slice(next, next + entry.reports.length);
|
|
30026
|
+
next += entry.reports.length;
|
|
30027
|
+
return {
|
|
30028
|
+
suite: entry.suite,
|
|
30029
|
+
name: entry.name,
|
|
30030
|
+
status: entry.status,
|
|
30031
|
+
time: entry.time,
|
|
30032
|
+
path,
|
|
30033
|
+
message: reports[0]?.message ?? null,
|
|
30034
|
+
detail: reports[0]?.detail ?? null,
|
|
30035
|
+
...reports.length > 1 ? { reports } : {}
|
|
30036
|
+
};
|
|
30037
|
+
});
|
|
30038
|
+
}
|
|
30013
30039
|
var MASKED_BRACKET = "[lb]";
|
|
30014
30040
|
function masked(expected, actual) {
|
|
30015
30041
|
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
@@ -33008,7 +33034,8 @@ function recordAt(path) {
|
|
|
33008
33034
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
33009
33035
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
33010
33036
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
33011
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
33037
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
33038
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
33012
33039
|
};
|
|
33013
33040
|
}
|
|
33014
33041
|
async function runningAs(pid) {
|
|
@@ -34053,7 +34080,7 @@ var TOOL_SPECS = [
|
|
|
34053
34080
|
},
|
|
34054
34081
|
{
|
|
34055
34082
|
name: "project_test",
|
|
34056
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
34083
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, every report under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
34057
34084
|
parameters: {
|
|
34058
34085
|
projectPath: PROJECT_PATH,
|
|
34059
34086
|
path: {
|
|
@@ -34497,7 +34524,7 @@ var TOOL_SPECS = [
|
|
|
34497
34524
|
},
|
|
34498
34525
|
{
|
|
34499
34526
|
name: "editor_output",
|
|
34500
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
34527
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
34501
34528
|
parameters: {
|
|
34502
34529
|
severity: {
|
|
34503
34530
|
type: "string",
|
|
@@ -34545,7 +34572,7 @@ var TOOL_SPECS = [
|
|
|
34545
34572
|
},
|
|
34546
34573
|
{
|
|
34547
34574
|
name: "editor_status",
|
|
34548
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
34575
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
|
|
34549
34576
|
parameters: {},
|
|
34550
34577
|
requires: []
|
|
34551
34578
|
},
|
|
@@ -35976,7 +36003,7 @@ function profileAnswer(reading, limit) {
|
|
|
35976
36003
|
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.`);
|
|
35977
36004
|
}
|
|
35978
36005
|
if (table.overheadDominated > 0 && table.perCallUs !== null) {
|
|
35979
|
-
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs:
|
|
36006
|
+
notes.push(`The profiler costs about ${table.perCallUs}µs a call in this run, measured on an empty function the runtime addon calls as it goes on, and that time lands in the self time of the function called. ${table.overheadDominated === 1 ? "One function has" : `${table.overheadDominated} functions have`} at least half ${table.overheadDominated === 1 ? "its" : "their"} selfMs in it, under overheadMs: that much of its time is the profiler's and goes once the profiler is off, whether the function is inlined or not. What inlining saves is the call itself, which costs something without the profiler too, not the work inside it.`);
|
|
35980
36007
|
}
|
|
35981
36008
|
notes.push(...cappedNotes(totals));
|
|
35982
36009
|
if (totals.unreadable > 0) {
|
|
@@ -37928,7 +37955,7 @@ class GodotServer {
|
|
|
37928
37955
|
isError: true
|
|
37929
37956
|
};
|
|
37930
37957
|
}
|
|
37931
|
-
const failed =
|
|
37958
|
+
const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
|
|
37932
37959
|
const orphans = orphansPrinted(said);
|
|
37933
37960
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
37934
37961
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -38429,7 +38456,9 @@ class GodotServer {
|
|
|
38429
38456
|
return {
|
|
38430
38457
|
pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
|
|
38431
38458
|
...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
|
|
38432
|
-
...run.pickedUp === true ? { adopted: true } : {}
|
|
38459
|
+
...run.pickedUp === true ? { adopted: true } : {},
|
|
38460
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
38461
|
+
...run.endedAt === undefined ? {} : { endedAt: new Date(run.endedAt).toISOString() }
|
|
38433
38462
|
};
|
|
38434
38463
|
}
|
|
38435
38464
|
async handleEditorStatus() {
|
|
@@ -39202,10 +39231,11 @@ class GodotServer {
|
|
|
39202
39231
|
return "unreached";
|
|
39203
39232
|
}
|
|
39204
39233
|
running.endedThrough = "editor";
|
|
39205
|
-
|
|
39206
|
-
|
|
39234
|
+
const stopped = announced === undefined ? await this.untilTheEditorStopsPlaying() : await untilGone([announced]);
|
|
39235
|
+
if (stopped) {
|
|
39236
|
+
running.endedAt = Date.now();
|
|
39207
39237
|
}
|
|
39208
|
-
return
|
|
39238
|
+
return stopped ? "gone" : "lingering";
|
|
39209
39239
|
}
|
|
39210
39240
|
if (running.pid === null) {
|
|
39211
39241
|
return "gone";
|
|
@@ -39254,6 +39284,7 @@ class GodotServer {
|
|
|
39254
39284
|
if (landed && gone) {
|
|
39255
39285
|
running.exitCode = null;
|
|
39256
39286
|
running.exitSignal = "SIGTERM";
|
|
39287
|
+
running.endedAt ??= Date.now();
|
|
39257
39288
|
}
|
|
39258
39289
|
if (landed) {
|
|
39259
39290
|
running.endedThrough = "signal";
|
|
@@ -39278,6 +39309,7 @@ class GodotServer {
|
|
|
39278
39309
|
if (asked === "signalled" && gone) {
|
|
39279
39310
|
run.exitCode = null;
|
|
39280
39311
|
run.exitSignal = "SIGTERM";
|
|
39312
|
+
run.endedAt ??= Date.now();
|
|
39281
39313
|
}
|
|
39282
39314
|
run.log.record(gone ? "info" : "warning", !gone ? `gdharness had the keeper holding pid ${pid} end it, and it was still running ${stopWaitMs() / 1000} seconds later.` : asked === "signalled" ? `gdharness ended pid ${pid} through the keeper holding it.` : `gdharness asked the keeper holding pid ${pid} to end it, and it had already gone.`);
|
|
39283
39315
|
return gone ? "gone" : "lingering";
|
|
@@ -39325,6 +39357,7 @@ class GodotServer {
|
|
|
39325
39357
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
39326
39358
|
started.exitCode = code;
|
|
39327
39359
|
started.exitSignal = code === null ? signal : null;
|
|
39360
|
+
started.endedAt = Date.now();
|
|
39328
39361
|
});
|
|
39329
39362
|
child.on("error", (err) => {
|
|
39330
39363
|
console.error("Failed to start Godot process:", err);
|
|
@@ -39639,6 +39672,9 @@ class GodotServer {
|
|
|
39639
39672
|
adopted.log.finish();
|
|
39640
39673
|
adopted.exitCode = record.exitCode ?? null;
|
|
39641
39674
|
adopted.exitSignal = record.exitSignal ?? null;
|
|
39675
|
+
if (record.endedAt !== undefined) {
|
|
39676
|
+
adopted.endedAt = record.endedAt;
|
|
39677
|
+
}
|
|
39642
39678
|
const exitMayBeComing = !alive(record.pid) && !exited(adopted);
|
|
39643
39679
|
if (!exitMayBeComing) {
|
|
39644
39680
|
adopted.endedUnwatched = !exited(adopted);
|
|
@@ -39932,6 +39968,9 @@ class GodotServer {
|
|
|
39932
39968
|
if (record !== null && record.pid === run.pid && (record.exitCode !== undefined || record.exitSignal !== undefined)) {
|
|
39933
39969
|
run.exitCode = record.exitCode ?? null;
|
|
39934
39970
|
run.exitSignal = record.exitSignal ?? null;
|
|
39971
|
+
if (record.endedAt !== undefined) {
|
|
39972
|
+
run.endedAt = record.endedAt;
|
|
39973
|
+
}
|
|
39935
39974
|
return true;
|
|
39936
39975
|
}
|
|
39937
39976
|
if (Date.now() >= until) {
|
|
@@ -40078,7 +40117,8 @@ class GodotServer {
|
|
|
40078
40117
|
project: run.throughEditor ? undefined : run.projectPath ?? undefined,
|
|
40079
40118
|
transcript: run.transcript ?? undefined,
|
|
40080
40119
|
startedAt: new Date(run.startedAt).toISOString(),
|
|
40081
|
-
|
|
40120
|
+
endedAt: stillRunning(run) || run.endedAt === undefined ? undefined : new Date(run.endedAt).toISOString(),
|
|
40121
|
+
elapsedMs: stillRunning(run) ? Date.now() - run.startedAt : run.endedAt === undefined ? undefined : run.endedAt - run.startedAt,
|
|
40082
40122
|
cpuSeconds,
|
|
40083
40123
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
40084
40124
|
omitted: selected.omitted,
|
package/build/keeper.js
CHANGED
|
@@ -421,7 +421,8 @@ function recordRunEnded(projectPath, pid, ending) {
|
|
|
421
421
|
const written = {
|
|
422
422
|
...record,
|
|
423
423
|
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
424
|
-
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
424
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal },
|
|
425
|
+
endedAt: Date.now()
|
|
425
426
|
};
|
|
426
427
|
try {
|
|
427
428
|
writeFileSync2(path, JSON.stringify(written, null, 2), "utf8");
|
|
@@ -457,7 +458,8 @@ function recordAt(path) {
|
|
|
457
458
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
458
459
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
459
460
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
460
|
-
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
461
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
462
|
+
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
|
461
463
|
};
|
|
462
464
|
}
|
|
463
465
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|