gdharness 1.0.23 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -16369,7 +16369,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16369
16369
|
},
|
|
16370
16370
|
{
|
|
16371
16371
|
name: "editor_output",
|
|
16372
|
-
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. 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 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.",
|
|
16372
|
+
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. 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.",
|
|
16373
16373
|
parameters: {
|
|
16374
16374
|
severity: {
|
|
16375
16375
|
type: "string",
|
|
@@ -16489,7 +16489,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16489
16489
|
property: {
|
|
16490
16490
|
type: "string",
|
|
16491
16491
|
ops: ["property", "find"],
|
|
16492
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A step that is not there is named, and says what was there instead.'
|
|
16492
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
16493
16493
|
},
|
|
16494
16494
|
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
16495
16495
|
includeProperties: {
|
|
@@ -16535,6 +16535,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
16535
16535
|
items: { type: "string" },
|
|
16536
16536
|
ops: ["metrics"],
|
|
16537
16537
|
description: "metrics: which to read. Default all."
|
|
16538
|
+
},
|
|
16539
|
+
timeoutMs: {
|
|
16540
|
+
type: "number",
|
|
16541
|
+
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
16538
16542
|
}
|
|
16539
16543
|
},
|
|
16540
16544
|
requires: [],
|
|
@@ -16562,11 +16566,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
16562
16566
|
},
|
|
16563
16567
|
{
|
|
16564
16568
|
name: "runtime_invoke",
|
|
16565
|
-
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
|
|
16569
|
+
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
16566
16570
|
parameters: {
|
|
16567
16571
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16568
16572
|
pid: RUNNING_PID,
|
|
16569
|
-
nodePath: {
|
|
16573
|
+
nodePath: {
|
|
16574
|
+
type: "string",
|
|
16575
|
+
ops: ["set", "call"],
|
|
16576
|
+
description: 'Absolute node path, such as "/root/Main/Player".'
|
|
16577
|
+
},
|
|
16570
16578
|
property: {
|
|
16571
16579
|
type: "string",
|
|
16572
16580
|
ops: ["set"],
|
|
@@ -16586,17 +16594,31 @@ var init_tool_definitions = __esm(() => {
|
|
|
16586
16594
|
type: "array",
|
|
16587
16595
|
ops: ["call"],
|
|
16588
16596
|
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
16597
|
+
},
|
|
16598
|
+
timeoutMs: {
|
|
16599
|
+
type: "number",
|
|
16600
|
+
ops: ["set", "call"],
|
|
16601
|
+
description: "set, call: how long to wait for the answer before answering pending with a requestId, which does not stop the call. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS. Give a longer one for a call known to take a while, such as one that plays many turns in one go."
|
|
16602
|
+
},
|
|
16603
|
+
requestId: {
|
|
16604
|
+
type: "number",
|
|
16605
|
+
ops: ["result"],
|
|
16606
|
+
description: "result: the requestId a pending answer gave."
|
|
16589
16607
|
}
|
|
16590
16608
|
},
|
|
16591
|
-
requires: [
|
|
16609
|
+
requires: [],
|
|
16592
16610
|
operations: {
|
|
16593
16611
|
set: {
|
|
16594
16612
|
summary: "set a property, on a node or on an object it holds",
|
|
16595
|
-
requires: ["property", "value"]
|
|
16613
|
+
requires: ["nodePath", "property", "value"]
|
|
16596
16614
|
},
|
|
16597
16615
|
call: {
|
|
16598
16616
|
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
16599
|
-
requires: ["method"]
|
|
16617
|
+
requires: ["nodePath", "method"]
|
|
16618
|
+
},
|
|
16619
|
+
result: {
|
|
16620
|
+
summary: "collect the reply to a set or call whose wait ran out, by the requestId it answered with",
|
|
16621
|
+
requires: ["requestId"]
|
|
16600
16622
|
}
|
|
16601
16623
|
}
|
|
16602
16624
|
},
|
|
@@ -16757,7 +16779,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16757
16779
|
says: {
|
|
16758
16780
|
type: "string",
|
|
16759
16781
|
ops: ["until"],
|
|
16760
|
-
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
|
|
16782
|
+
description: "until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took."
|
|
16761
16783
|
},
|
|
16762
16784
|
includeHidden: {
|
|
16763
16785
|
type: "boolean",
|
|
@@ -31916,14 +31938,40 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
31916
31938
|
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass pid to choose one, or projectPath when the project you mean is running only one.`
|
|
31917
31939
|
};
|
|
31918
31940
|
}
|
|
31919
|
-
function
|
|
31941
|
+
function keepLate(id, answer) {
|
|
31942
|
+
lateAnswers.delete(id);
|
|
31943
|
+
lateAnswers.set(id, answer);
|
|
31944
|
+
while (lateAnswers.size > LATE_KEPT) {
|
|
31945
|
+
const oldest = lateAnswers.keys().next().value;
|
|
31946
|
+
if (oldest === undefined) {
|
|
31947
|
+
break;
|
|
31948
|
+
}
|
|
31949
|
+
lateAnswers.delete(oldest);
|
|
31950
|
+
}
|
|
31951
|
+
}
|
|
31952
|
+
function lateAnswer(id) {
|
|
31953
|
+
return lateAnswers.get(id);
|
|
31954
|
+
}
|
|
31955
|
+
function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = false) {
|
|
31920
31956
|
const id = nextRequestId++;
|
|
31957
|
+
const sentAt = Date.now();
|
|
31921
31958
|
return new Promise((settle) => {
|
|
31922
31959
|
let done = false;
|
|
31960
|
+
let late = false;
|
|
31923
31961
|
let buffered = "";
|
|
31924
31962
|
let welcomed = false;
|
|
31925
31963
|
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
31964
|
+
let lateTimer = null;
|
|
31926
31965
|
const finish = (reply) => {
|
|
31966
|
+
if (late) {
|
|
31967
|
+
if (lateTimer !== null) {
|
|
31968
|
+
clearTimeout(lateTimer);
|
|
31969
|
+
}
|
|
31970
|
+
socket.destroy();
|
|
31971
|
+
keepLate(id, reply.ok || reply.reason === "error" ? { state: "answered", command, sentAt, answeredAt: Date.now(), reply } : { state: "never", command, sentAt, why: reply.message });
|
|
31972
|
+
late = false;
|
|
31973
|
+
return;
|
|
31974
|
+
}
|
|
31927
31975
|
if (done) {
|
|
31928
31976
|
return;
|
|
31929
31977
|
}
|
|
@@ -31933,10 +31981,32 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
31933
31981
|
settle(reply);
|
|
31934
31982
|
};
|
|
31935
31983
|
const timer = setTimeout(() => {
|
|
31936
|
-
|
|
31984
|
+
const still = welcomed ? `The game (${describe2(endpoint)}) has the request and did not answer '${command}' within ${timeoutMs}ms.` : `The game (${describe2(endpoint)}) did not answer '${command}' within ${timeoutMs}ms and had not taken the request: its frame has not ended since it was sent, which a long frame or a breakpoint does, and the request runs when it does.`;
|
|
31985
|
+
if (!keepListening) {
|
|
31986
|
+
finish({
|
|
31987
|
+
ok: false,
|
|
31988
|
+
reason: "busy",
|
|
31989
|
+
message: `${still} It is either working through something long or held at a breakpoint.`
|
|
31990
|
+
});
|
|
31991
|
+
return;
|
|
31992
|
+
}
|
|
31993
|
+
done = true;
|
|
31994
|
+
late = true;
|
|
31995
|
+
keepLate(id, { state: "waiting", command, sentAt });
|
|
31996
|
+
lateTimer = setTimeout(() => {
|
|
31997
|
+
finish({
|
|
31998
|
+
ok: false,
|
|
31999
|
+
reason: "busy",
|
|
32000
|
+
message: `No answer came within ${LATE_ANSWER_MS / 60000} minutes.`
|
|
32001
|
+
});
|
|
32002
|
+
}, LATE_ANSWER_MS);
|
|
32003
|
+
lateTimer.unref();
|
|
32004
|
+
socket.unref();
|
|
32005
|
+
settle({
|
|
31937
32006
|
ok: false,
|
|
31938
32007
|
reason: "busy",
|
|
31939
|
-
|
|
32008
|
+
requestId: id,
|
|
32009
|
+
message: `${still} Nothing was cancelled: a call that takes longer than the wait goes on and does everything it was asked, so read the state back rather than repeating it. runtime_invoke op result with requestId ${id} answers with its reply once it comes, and timeoutMs waits longer next time. A game held at a breakpoint instead is one debug_state stack answers about.`
|
|
31940
32010
|
});
|
|
31941
32011
|
}, timeoutMs);
|
|
31942
32012
|
socket.setEncoding("utf8");
|
|
@@ -31948,7 +32018,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
31948
32018
|
buffered += chunk;
|
|
31949
32019
|
let newline = buffered.indexOf(`
|
|
31950
32020
|
`);
|
|
31951
|
-
while (newline !== -1 && !done) {
|
|
32021
|
+
while (newline !== -1 && (!done || late)) {
|
|
31952
32022
|
const line = buffered.slice(0, newline).trim();
|
|
31953
32023
|
buffered = buffered.slice(newline + 1);
|
|
31954
32024
|
newline = buffered.indexOf(`
|
|
@@ -32003,7 +32073,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
32003
32073
|
});
|
|
32004
32074
|
});
|
|
32005
32075
|
}
|
|
32006
|
-
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
32076
|
+
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
|
|
32007
32077
|
var init_runtime_client = __esm(() => {
|
|
32008
32078
|
init_paths();
|
|
32009
32079
|
init_process_children();
|
|
@@ -32012,6 +32082,8 @@ var init_runtime_client = __esm(() => {
|
|
|
32012
32082
|
ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
32013
32083
|
gone = [];
|
|
32014
32084
|
verdicts = new Map;
|
|
32085
|
+
LATE_ANSWER_MS = 30 * 60000;
|
|
32086
|
+
lateAnswers = new Map;
|
|
32015
32087
|
});
|
|
32016
32088
|
|
|
32017
32089
|
// src/boot-note.ts
|
|
@@ -36959,6 +37031,65 @@ function whyNoReport(printed, asked) {
|
|
|
36959
37031
|
}
|
|
36960
37032
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
36961
37033
|
}
|
|
37034
|
+
function unspelled(text) {
|
|
37035
|
+
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
37036
|
+
}
|
|
37037
|
+
function readDiff(printed) {
|
|
37038
|
+
let merged = "";
|
|
37039
|
+
let actual = "";
|
|
37040
|
+
let background = null;
|
|
37041
|
+
let last = 0;
|
|
37042
|
+
const take = (text) => {
|
|
37043
|
+
merged += text;
|
|
37044
|
+
if (background === null) {
|
|
37045
|
+
actual += text;
|
|
37046
|
+
} else if (background === "added") {
|
|
37047
|
+
actual += unspelled(text);
|
|
37048
|
+
}
|
|
37049
|
+
};
|
|
37050
|
+
for (const match of printed.matchAll(COLOUR2)) {
|
|
37051
|
+
take(printed.slice(last, match.index));
|
|
37052
|
+
last = match.index + match[0].length;
|
|
37053
|
+
const codes = (match[1] ?? "").split(";");
|
|
37054
|
+
if (codes[0] === "48" && codes[1] === "2") {
|
|
37055
|
+
background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
|
|
37056
|
+
} else if (codes[0] === "0" || codes[0] === "") {
|
|
37057
|
+
background = null;
|
|
37058
|
+
}
|
|
37059
|
+
}
|
|
37060
|
+
take(printed.slice(last));
|
|
37061
|
+
return { merged, actual };
|
|
37062
|
+
}
|
|
37063
|
+
function withActualsPrinted(failed, printed) {
|
|
37064
|
+
const diffs = [...printed.replaceAll(`\r
|
|
37065
|
+
`, `
|
|
37066
|
+
`).matchAll(PRINTED_ACTUAL)].map((match) => ({
|
|
37067
|
+
at: match[3] ?? "",
|
|
37068
|
+
...readDiff(match[1] ?? "")
|
|
37069
|
+
}));
|
|
37070
|
+
return failed.map((entry) => {
|
|
37071
|
+
if (entry.detail === null) {
|
|
37072
|
+
return entry;
|
|
37073
|
+
}
|
|
37074
|
+
let detail = entry.detail;
|
|
37075
|
+
for (const diff of diffs) {
|
|
37076
|
+
const wrong = ` but was
|
|
37077
|
+
'${diff.merged.replaceAll("\r", "")}'
|
|
37078
|
+
at ${diff.at}`;
|
|
37079
|
+
if (detail.includes(wrong)) {
|
|
37080
|
+
detail = detail.replace(wrong, () => ` but was
|
|
37081
|
+
'${diff.actual}'
|
|
37082
|
+
at ${diff.at}`);
|
|
37083
|
+
}
|
|
37084
|
+
}
|
|
37085
|
+
const alike = /Expecting:\n '([\s\S]*)'\n but was\n '\1'\n\tat /.test(detail);
|
|
37086
|
+
if (alike) {
|
|
37087
|
+
detail = `${detail}
|
|
37088
|
+
${BOTH_SIDES_ALIKE_NOTE}`;
|
|
37089
|
+
}
|
|
37090
|
+
return detail === entry.detail ? entry : { ...entry, detail };
|
|
37091
|
+
});
|
|
37092
|
+
}
|
|
36962
37093
|
function orphansPrinted(printed) {
|
|
36963
37094
|
const suites = [];
|
|
36964
37095
|
let total = null;
|
|
@@ -36989,7 +37120,7 @@ function orphansPrinted(printed) {
|
|
|
36989
37120
|
suites
|
|
36990
37121
|
};
|
|
36991
37122
|
}
|
|
36992
|
-
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
37123
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, ESCAPE, PRINTED_ACTUAL, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
36993
37124
|
var init_junit = __esm(() => {
|
|
36994
37125
|
MalformedReportError = class MalformedReportError extends Error {
|
|
36995
37126
|
constructor(message) {
|
|
@@ -37001,6 +37132,21 @@ var init_junit = __esm(() => {
|
|
|
37001
37132
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
37002
37133
|
NOTHING_RAN = /no test cases found/i;
|
|
37003
37134
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
|
|
37135
|
+
ESCAPE = String.fromCharCode(27);
|
|
37136
|
+
PRINTED_ACTUAL = new RegExp(` but was\\n '([\\s\\S]*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat ('[^'\\n]*' in \\S+:\\d+)`, "g");
|
|
37137
|
+
COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
37138
|
+
SPELLED = {
|
|
37139
|
+
"<LF>": `
|
|
37140
|
+
`,
|
|
37141
|
+
"<CR>": "\r",
|
|
37142
|
+
"<TAB>": "\t",
|
|
37143
|
+
"<BS>": "\b",
|
|
37144
|
+
"<FF>": "\f",
|
|
37145
|
+
"<VT>": "\v",
|
|
37146
|
+
"<BEL>": "\x07",
|
|
37147
|
+
"<ESC>": ESCAPE,
|
|
37148
|
+
"<DEL>": ""
|
|
37149
|
+
};
|
|
37004
37150
|
RUNNING_SUITE = /Run Test Suite:\s*(\S+)/;
|
|
37005
37151
|
ORPHANS = /(\d+)\s+orphans/;
|
|
37006
37152
|
});
|
|
@@ -37811,14 +37957,19 @@ function editorNoteAt(path) {
|
|
|
37811
37957
|
}
|
|
37812
37958
|
return { projectPath, transcript, startedAt };
|
|
37813
37959
|
}
|
|
37814
|
-
function recordRunEnded(pid,
|
|
37960
|
+
function recordRunEnded(pid, ending) {
|
|
37815
37961
|
const path = recordPath();
|
|
37816
37962
|
const record = recordAt(path);
|
|
37817
37963
|
if (record === null || record.pid !== pid) {
|
|
37818
37964
|
return;
|
|
37819
37965
|
}
|
|
37966
|
+
const written = {
|
|
37967
|
+
...record,
|
|
37968
|
+
...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
|
|
37969
|
+
...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
|
|
37970
|
+
};
|
|
37820
37971
|
try {
|
|
37821
|
-
writeFileSync15(path, JSON.stringify(
|
|
37972
|
+
writeFileSync15(path, JSON.stringify(written, null, 2), "utf8");
|
|
37822
37973
|
} catch {}
|
|
37823
37974
|
}
|
|
37824
37975
|
function readRunRecord() {
|
|
@@ -37855,7 +38006,8 @@ function recordAt(path) {
|
|
|
37855
38006
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
37856
38007
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
37857
38008
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
37858
|
-
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {}
|
|
38009
|
+
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
38010
|
+
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
|
|
37859
38011
|
};
|
|
37860
38012
|
}
|
|
37861
38013
|
function runningAs(pid) {
|
|
@@ -38116,8 +38268,21 @@ function describeHalt(halt) {
|
|
|
38116
38268
|
return `stopped with reason "${halt.reason}"`;
|
|
38117
38269
|
}
|
|
38118
38270
|
}
|
|
38271
|
+
function exited(run) {
|
|
38272
|
+
return run.exitCode !== null || run.exitSignal !== null;
|
|
38273
|
+
}
|
|
38274
|
+
function endingOf(failed) {
|
|
38275
|
+
return {
|
|
38276
|
+
exitCode: typeof failed.code === "number" ? failed.code : null,
|
|
38277
|
+
exitSignal: typeof failed.signal === "string" ? failed.signal : null,
|
|
38278
|
+
failure: typeof failed.code === "string" ? failed.code : null
|
|
38279
|
+
};
|
|
38280
|
+
}
|
|
38281
|
+
function howItExited(run) {
|
|
38282
|
+
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
|
|
38283
|
+
}
|
|
38119
38284
|
function stillRunning(run) {
|
|
38120
|
-
if (run
|
|
38285
|
+
if (run === null || exited(run) || run.endedUnwatched === true) {
|
|
38121
38286
|
return false;
|
|
38122
38287
|
}
|
|
38123
38288
|
if (typeof run.endedHere === "string") {
|
|
@@ -38147,7 +38312,7 @@ function runIsUp(run, editorSays, now = Date.now()) {
|
|
|
38147
38312
|
return editorSays ?? stillRunning(run);
|
|
38148
38313
|
}
|
|
38149
38314
|
function noCodeWillCome(run) {
|
|
38150
|
-
return run
|
|
38315
|
+
return !exited(run) && run.process === null;
|
|
38151
38316
|
}
|
|
38152
38317
|
async function untilGone(handle, pids, withinMs = STOP_WAIT_MS) {
|
|
38153
38318
|
const gone = () => (handle?.exitCode !== null || handle.signalCode !== null) && pids.every((pid) => !alive(pid));
|
|
@@ -38980,7 +39145,8 @@ class GodotServer {
|
|
|
38980
39145
|
return await this.handleEditorStatus();
|
|
38981
39146
|
case "editor_rescan":
|
|
38982
39147
|
return await this.handleRescanFilesystem(args);
|
|
38983
|
-
case "runtime_inspect":
|
|
39148
|
+
case "runtime_inspect": {
|
|
39149
|
+
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
38984
39150
|
switch (op) {
|
|
38985
39151
|
case "tree":
|
|
38986
39152
|
return await this.handleRuntimeCommand("get_tree", {
|
|
@@ -38989,45 +39155,45 @@ class GodotServer {
|
|
|
38989
39155
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
38990
39156
|
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
38991
39157
|
properties: readArray(args, "properties") ?? []
|
|
38992
|
-
});
|
|
39158
|
+
}, patience);
|
|
38993
39159
|
case "find":
|
|
38994
|
-
return await this.handleFindRuntimeNodes(args);
|
|
39160
|
+
return await this.handleFindRuntimeNodes(args, patience);
|
|
38995
39161
|
case "text":
|
|
38996
39162
|
return await this.handleRuntimeCommand("read_text", {
|
|
38997
39163
|
...whichGame(args),
|
|
38998
39164
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
38999
39165
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
39000
39166
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
39001
|
-
});
|
|
39167
|
+
}, patience);
|
|
39002
39168
|
case "rect":
|
|
39003
|
-
return await this.handleRuntimeCommand("get_rect", {
|
|
39004
|
-
...whichGame(args),
|
|
39005
|
-
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
39006
|
-
});
|
|
39169
|
+
return await this.handleRuntimeCommand("get_rect", { ...whichGame(args), path: readNonEmptyString(args, "nodePath") ?? "" }, patience);
|
|
39007
39170
|
case "property":
|
|
39008
39171
|
return await this.handleRuntimeCommand("get_property", {
|
|
39009
39172
|
...whichGame(args),
|
|
39010
39173
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
39011
39174
|
property: readNonEmptyString(args, "property") ?? ""
|
|
39012
|
-
});
|
|
39175
|
+
}, patience);
|
|
39013
39176
|
default:
|
|
39014
|
-
return await this.handleRuntimeCommand("get_metrics", {
|
|
39015
|
-
|
|
39016
|
-
|
|
39017
|
-
|
|
39177
|
+
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, patience);
|
|
39178
|
+
}
|
|
39179
|
+
}
|
|
39180
|
+
case "runtime_invoke": {
|
|
39181
|
+
if (op === "result") {
|
|
39182
|
+
return this.handleLateAnswer(args);
|
|
39018
39183
|
}
|
|
39019
|
-
|
|
39184
|
+
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
39020
39185
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
39021
39186
|
...whichGame(args),
|
|
39022
39187
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
39023
39188
|
property: readString(args, "property") ?? "",
|
|
39024
39189
|
value: args["value"]
|
|
39025
|
-
}) : await this.handleRuntimeCommand("call_method", {
|
|
39190
|
+
}, patience) : await this.handleRuntimeCommand("call_method", {
|
|
39026
39191
|
...whichGame(args),
|
|
39027
39192
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
39028
39193
|
method: readString(args, "method") ?? "",
|
|
39029
39194
|
args: readArray(args, "args") ?? []
|
|
39030
|
-
});
|
|
39195
|
+
}, patience);
|
|
39196
|
+
}
|
|
39031
39197
|
case "runtime_capture":
|
|
39032
39198
|
return await this.handleRuntimeCommand(op === "screenshot" ? "capture_screenshot" : "capture_viewport", args);
|
|
39033
39199
|
case "runtime_input":
|
|
@@ -39178,10 +39344,10 @@ class GodotServer {
|
|
|
39178
39344
|
}
|
|
39179
39345
|
async debuggedGame(intent) {
|
|
39180
39346
|
const game = this.activeProcess;
|
|
39181
|
-
if (game !== null && game
|
|
39347
|
+
if (game !== null && exited(game)) {
|
|
39182
39348
|
return {
|
|
39183
39349
|
ok: false,
|
|
39184
|
-
response: this.createErrorResponse(`The last run has already finished (
|
|
39350
|
+
response: this.createErrorResponse(`The last run has already finished (${howItExited(game)}), so there is no debug session to answer for.`, [
|
|
39185
39351
|
"editor_output still reads what it printed, until the next run starts",
|
|
39186
39352
|
"editor_run start plays another, which the debugger can hold"
|
|
39187
39353
|
])
|
|
@@ -39455,10 +39621,11 @@ class GodotServer {
|
|
|
39455
39621
|
];
|
|
39456
39622
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
39457
39623
|
const log = new GameLog;
|
|
39458
|
-
let
|
|
39624
|
+
let ending = { exitCode: 0, exitSignal: null, failure: null };
|
|
39459
39625
|
try {
|
|
39460
39626
|
const { stdout, stderr } = await run7(engine.value, exportArgs, {
|
|
39461
39627
|
timeout: 300000,
|
|
39628
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
39462
39629
|
signal: callSignal()
|
|
39463
39630
|
});
|
|
39464
39631
|
log.append("stdout", stdout);
|
|
@@ -39470,26 +39637,32 @@ class GodotServer {
|
|
|
39470
39637
|
const failed = error;
|
|
39471
39638
|
log.append("stdout", failed.stdout);
|
|
39472
39639
|
log.append("stderr", failed.stderr);
|
|
39473
|
-
|
|
39640
|
+
ending = endingOf(failed);
|
|
39474
39641
|
} finally {
|
|
39475
39642
|
discard(exportLogs);
|
|
39476
39643
|
}
|
|
39477
39644
|
log.finish();
|
|
39478
39645
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
39479
39646
|
const verdict = {
|
|
39480
|
-
exported: exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
|
|
39647
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
|
|
39481
39648
|
preset,
|
|
39482
39649
|
outputPath: output.relativePath,
|
|
39483
39650
|
debug,
|
|
39484
|
-
exitCode,
|
|
39651
|
+
exitCode: ending.exitCode,
|
|
39652
|
+
exitSignal: ending.exitSignal ?? undefined,
|
|
39653
|
+
failure: ending.failure ?? undefined,
|
|
39485
39654
|
errors: log.count("error"),
|
|
39486
39655
|
warnings: log.count("warning"),
|
|
39487
39656
|
entries: forAnswer(problems.entries)
|
|
39488
39657
|
};
|
|
39489
39658
|
if (!verdict.exported) {
|
|
39659
|
+
const why = ending.exitSignal === null ? ending.failure === null ? "" : ` The engine was ended by Node (${ending.failure}), so it has no exit code.` : ` The engine was ended by ${ending.exitSignal}, so it has no exit code: the export ran past its five minutes or the call was cancelled.`;
|
|
39490
39660
|
return {
|
|
39491
39661
|
content: [
|
|
39492
|
-
{
|
|
39662
|
+
{
|
|
39663
|
+
type: "text",
|
|
39664
|
+
text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
|
|
39665
|
+
},
|
|
39493
39666
|
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
39494
39667
|
],
|
|
39495
39668
|
isError: true
|
|
@@ -39544,6 +39717,10 @@ class GodotServer {
|
|
|
39544
39717
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
39545
39718
|
const userData = mkdtempSync3(join23(tmpdir5(), "gdharness-tests-"));
|
|
39546
39719
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
39720
|
+
const console2 = [];
|
|
39721
|
+
run.process.stdout?.on("data", (data) => {
|
|
39722
|
+
console2.push(data);
|
|
39723
|
+
});
|
|
39547
39724
|
const hung = await new Promise((resolve) => {
|
|
39548
39725
|
const timer = setTimeout(() => {
|
|
39549
39726
|
run.process.kill();
|
|
@@ -39588,7 +39765,7 @@ class GodotServer {
|
|
|
39588
39765
|
const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
|
|
39589
39766
|
const said = printed.map((entry) => entry.text);
|
|
39590
39767
|
const nothingRan = hung ? null : whyNoReport(said, asked);
|
|
39591
|
-
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ??
|
|
39768
|
+
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
39592
39769
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
39593
39770
|
if (report === null) {
|
|
39594
39771
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
@@ -39602,6 +39779,7 @@ class GodotServer {
|
|
|
39602
39779
|
verdict,
|
|
39603
39780
|
tests: 0,
|
|
39604
39781
|
exitCode,
|
|
39782
|
+
exitSignal: run.exitSignal ?? undefined,
|
|
39605
39783
|
hung,
|
|
39606
39784
|
arguments: cmdArgs,
|
|
39607
39785
|
entries: forAnswer(printed.slice(0, 60)),
|
|
@@ -39612,7 +39790,7 @@ class GodotServer {
|
|
|
39612
39790
|
isError: true
|
|
39613
39791
|
};
|
|
39614
39792
|
}
|
|
39615
|
-
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
39793
|
+
const failed = withActualsPrinted(report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path }))), Buffer.concat(console2).toString("utf8"));
|
|
39616
39794
|
const orphans = orphansPrinted(said);
|
|
39617
39795
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
39618
39796
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -39629,6 +39807,7 @@ class GodotServer {
|
|
|
39629
39807
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
39630
39808
|
verdict,
|
|
39631
39809
|
exitCode,
|
|
39810
|
+
exitSignal: run.exitSignal ?? undefined,
|
|
39632
39811
|
tests: report.tests,
|
|
39633
39812
|
failures: report.failures,
|
|
39634
39813
|
errors: report.errors,
|
|
@@ -40360,6 +40539,7 @@ class GodotServer {
|
|
|
40360
40539
|
projectPath,
|
|
40361
40540
|
startedAt,
|
|
40362
40541
|
exitCode: null,
|
|
40542
|
+
exitSignal: null,
|
|
40363
40543
|
throughEditor: true,
|
|
40364
40544
|
brokeOn: null,
|
|
40365
40545
|
announcedBefore: alreadyPlaying,
|
|
@@ -40513,6 +40693,7 @@ class GodotServer {
|
|
|
40513
40693
|
projectPath: null,
|
|
40514
40694
|
startedAt: Date.now(),
|
|
40515
40695
|
exitCode: null,
|
|
40696
|
+
exitSignal: null,
|
|
40516
40697
|
throughEditor: false,
|
|
40517
40698
|
brokeOn: null
|
|
40518
40699
|
};
|
|
@@ -40522,13 +40703,11 @@ class GodotServer {
|
|
|
40522
40703
|
child.stderr.on("data", (data) => {
|
|
40523
40704
|
log.append("stderr", data);
|
|
40524
40705
|
});
|
|
40525
|
-
child.on("exit", (code) => {
|
|
40526
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
40706
|
+
child.on("exit", (code, signal) => {
|
|
40707
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
40527
40708
|
log.finish();
|
|
40528
|
-
started.exitCode = code
|
|
40529
|
-
|
|
40530
|
-
recordRunEnded(started.pid, started.exitCode);
|
|
40531
|
-
}
|
|
40709
|
+
started.exitCode = code;
|
|
40710
|
+
started.exitSignal = code === null ? signal : null;
|
|
40532
40711
|
});
|
|
40533
40712
|
child.on("error", (err) => {
|
|
40534
40713
|
console.error("Failed to start Godot process:", err);
|
|
@@ -40562,14 +40741,19 @@ class GodotServer {
|
|
|
40562
40741
|
projectPath,
|
|
40563
40742
|
startedAt,
|
|
40564
40743
|
exitCode: null,
|
|
40744
|
+
exitSignal: null,
|
|
40565
40745
|
throughEditor: false,
|
|
40566
40746
|
brokeOn: null
|
|
40567
40747
|
};
|
|
40568
|
-
child.on("exit", (code) => {
|
|
40569
|
-
this.logDebug(`Godot process exited with code ${code ?? "none"}`);
|
|
40748
|
+
child.on("exit", (code, signal) => {
|
|
40749
|
+
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
40570
40750
|
this.drainTranscript(started);
|
|
40571
40751
|
log.finish();
|
|
40572
|
-
started.exitCode = code
|
|
40752
|
+
started.exitCode = code;
|
|
40753
|
+
started.exitSignal = code === null ? signal : null;
|
|
40754
|
+
if (started.pid !== null) {
|
|
40755
|
+
recordRunEnded(started.pid, { exitCode: started.exitCode, exitSignal: started.exitSignal });
|
|
40756
|
+
}
|
|
40573
40757
|
});
|
|
40574
40758
|
child.on("error", (err) => {
|
|
40575
40759
|
console.error("Failed to start Godot process:", err);
|
|
@@ -40652,6 +40836,7 @@ class GodotServer {
|
|
|
40652
40836
|
projectPath: project,
|
|
40653
40837
|
startedAt: note?.startedAt ?? Date.now(),
|
|
40654
40838
|
exitCode: null,
|
|
40839
|
+
exitSignal: null,
|
|
40655
40840
|
throughEditor: true,
|
|
40656
40841
|
brokeOn: null,
|
|
40657
40842
|
pickedUpPlaying: true
|
|
@@ -40788,6 +40973,7 @@ class GodotServer {
|
|
|
40788
40973
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
40789
40974
|
startedAt: record.startedAt,
|
|
40790
40975
|
exitCode: null,
|
|
40976
|
+
exitSignal: null,
|
|
40791
40977
|
throughEditor: false,
|
|
40792
40978
|
brokeOn: null
|
|
40793
40979
|
};
|
|
@@ -40795,7 +40981,8 @@ class GodotServer {
|
|
|
40795
40981
|
if (!alive(record.pid) || !couldStillBeTheRecordedRun(record)) {
|
|
40796
40982
|
adopted.log.finish();
|
|
40797
40983
|
adopted.exitCode = record.exitCode ?? null;
|
|
40798
|
-
adopted.
|
|
40984
|
+
adopted.exitSignal = record.exitSignal ?? null;
|
|
40985
|
+
adopted.endedUnwatched = !exited(adopted);
|
|
40799
40986
|
}
|
|
40800
40987
|
this.activeProcess = adopted;
|
|
40801
40988
|
return adopted;
|
|
@@ -40894,6 +41081,7 @@ class GodotServer {
|
|
|
40894
41081
|
booted: !hung && boot.exitCode === 0 && errors === 0,
|
|
40895
41082
|
hung,
|
|
40896
41083
|
exitCode: boot.exitCode,
|
|
41084
|
+
exitSignal: boot.exitSignal ?? undefined,
|
|
40897
41085
|
durationMs: Date.now() - boot.startedAt,
|
|
40898
41086
|
frames,
|
|
40899
41087
|
errors,
|
|
@@ -40920,7 +41108,11 @@ class GodotServer {
|
|
|
40920
41108
|
}
|
|
40921
41109
|
async runStillGoing(run, editorSays) {
|
|
40922
41110
|
if (!run.throughEditor) {
|
|
40923
|
-
|
|
41111
|
+
const going = stillRunning(run);
|
|
41112
|
+
if (!going && run.process !== null && !exited(run)) {
|
|
41113
|
+
await untilGone(run.process, [], EXIT_REPORTED_WITHIN_MS);
|
|
41114
|
+
}
|
|
41115
|
+
return going;
|
|
40924
41116
|
}
|
|
40925
41117
|
const announced = this.announcedPidOf(run);
|
|
40926
41118
|
if (announced !== undefined && !alive(announced)) {
|
|
@@ -41028,8 +41220,8 @@ class GodotServer {
|
|
|
41028
41220
|
}
|
|
41029
41221
|
if (typeof run.endedHere === "string") {
|
|
41030
41222
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
41031
|
-
} else if (!stillRunning(run) && !run.throughEditor && run
|
|
41032
|
-
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
41223
|
+
} else if (!stillRunning(run) && !run.throughEditor && exited(run)) {
|
|
41224
|
+
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : run.exitSignal === null ? "Nothing here ended this run: it stopped on its own or something outside this server stopped it." : `Nothing here ended this run: something outside this server sent it ${run.exitSignal}, so it has no exit code.`);
|
|
41033
41225
|
}
|
|
41034
41226
|
if (run.endedUnwatched === true) {
|
|
41035
41227
|
notes.push(endedWithoutACode({
|
|
@@ -41052,6 +41244,7 @@ class GodotServer {
|
|
|
41052
41244
|
return this.jsonTextResponse({
|
|
41053
41245
|
running: going,
|
|
41054
41246
|
exitCode: run.exitCode,
|
|
41247
|
+
exitSignal: run.exitSignal ?? undefined,
|
|
41055
41248
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
41056
41249
|
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
41057
41250
|
errors: run.log.count("error"),
|
|
@@ -41105,10 +41298,11 @@ class GodotServer {
|
|
|
41105
41298
|
},
|
|
41106
41299
|
exitedBeforeStop: !wasRunning,
|
|
41107
41300
|
exitCode: stopped.exitCode,
|
|
41301
|
+
exitSignal: stopped.exitSignal ?? undefined,
|
|
41108
41302
|
errors: stopped.log.count("error"),
|
|
41109
41303
|
warnings: stopped.log.count("warning"),
|
|
41110
41304
|
clean: stopped.log.count("error") === 0,
|
|
41111
|
-
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${gone ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
41305
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : `${stopped.throughEditor ? endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stopped.exitSignal === null ? "" : ` It was ended by ${stopped.exitSignal} and so has no exit code.`}${gone ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`} ${aboutTheChildren(ended, stopped.throughEditor)}`,
|
|
41112
41306
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
41113
41307
|
});
|
|
41114
41308
|
}
|
|
@@ -41524,7 +41718,17 @@ class GodotServer {
|
|
|
41524
41718
|
const screenshotDir = expectsScreenshot ? mkdtempSync3(join23(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
41525
41719
|
const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
|
|
41526
41720
|
try {
|
|
41527
|
-
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
41721
|
+
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
41722
|
+
if (!reply.ok && reply.requestId !== undefined) {
|
|
41723
|
+
return this.jsonTextResponse({
|
|
41724
|
+
pending: true,
|
|
41725
|
+
requestId: reply.requestId,
|
|
41726
|
+
command,
|
|
41727
|
+
waitedMs: timeoutMs,
|
|
41728
|
+
note: reply.message,
|
|
41729
|
+
...answeredBy
|
|
41730
|
+
});
|
|
41731
|
+
}
|
|
41528
41732
|
if (!reply.ok) {
|
|
41529
41733
|
return this.createErrorResponse(reply.message);
|
|
41530
41734
|
}
|
|
@@ -41560,7 +41764,36 @@ class GodotServer {
|
|
|
41560
41764
|
discard(screenshotDir);
|
|
41561
41765
|
}
|
|
41562
41766
|
}
|
|
41563
|
-
|
|
41767
|
+
handleLateAnswer(args) {
|
|
41768
|
+
const requestId = readPositiveNumber(args, "requestId") ?? 0;
|
|
41769
|
+
const late = lateAnswer(requestId);
|
|
41770
|
+
if (late === undefined) {
|
|
41771
|
+
return this.createErrorResponse(`No request ${requestId} is waiting to be collected here: an id is kept only from a pending answer this server gave, and only for its fifty most recent.`);
|
|
41772
|
+
}
|
|
41773
|
+
if (late.state === "waiting") {
|
|
41774
|
+
return this.jsonTextResponse({
|
|
41775
|
+
pending: true,
|
|
41776
|
+
requestId,
|
|
41777
|
+
command: late.command,
|
|
41778
|
+
sentAgoMs: Date.now() - late.sentAt,
|
|
41779
|
+
note: "The game has not answered it yet. It is still being listened for, and this answers with the reply once it comes."
|
|
41780
|
+
});
|
|
41781
|
+
}
|
|
41782
|
+
if (late.state === "never") {
|
|
41783
|
+
return this.createErrorResponse(`Request ${requestId} ('${late.command}') got no answer: ${late.why} Read the state back to see whether it ran.`);
|
|
41784
|
+
}
|
|
41785
|
+
const reply = late.reply;
|
|
41786
|
+
if (!reply.ok) {
|
|
41787
|
+
return this.createErrorResponse(`Request ${requestId} ('${late.command}') was answered with a refusal: ${reply.message}`);
|
|
41788
|
+
}
|
|
41789
|
+
const { id: _id, ...payload } = reply.payload;
|
|
41790
|
+
return this.jsonTextResponse({
|
|
41791
|
+
...payload,
|
|
41792
|
+
requestId,
|
|
41793
|
+
answeredAfterMs: late.answeredAt - late.sentAt
|
|
41794
|
+
});
|
|
41795
|
+
}
|
|
41796
|
+
async handleFindRuntimeNodes(args, patience) {
|
|
41564
41797
|
const filters = {};
|
|
41565
41798
|
const className = readNonEmptyString(args, "className");
|
|
41566
41799
|
const script = readNonEmptyString(args, "script");
|
|
@@ -41589,7 +41822,7 @@ class GodotServer {
|
|
|
41589
41822
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
41590
41823
|
...property === undefined ? {} : { property },
|
|
41591
41824
|
...includeHidden === undefined ? {} : { include_hidden: includeHidden }
|
|
41592
|
-
});
|
|
41825
|
+
}, patience);
|
|
41593
41826
|
}
|
|
41594
41827
|
async handleRuntimeWait(op, args) {
|
|
41595
41828
|
if (op === "frames") {
|
|
@@ -41624,7 +41857,7 @@ async function runGodotServer() {
|
|
|
41624
41857
|
const server = new GodotServer;
|
|
41625
41858
|
await server.run();
|
|
41626
41859
|
}
|
|
41627
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
41860
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
41628
41861
|
var init_server3 = __esm(() => {
|
|
41629
41862
|
init_mcp();
|
|
41630
41863
|
init_stdio2();
|