gdharness 1.1.44 → 1.1.46
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 +64 -26
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +97 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +42 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +160 -41
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +146 -6
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +6 -2
- package/build/index.js +63 -25
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17588,13 +17588,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
17588
17588
|
},
|
|
17589
17589
|
{
|
|
17590
17590
|
name: "project_test",
|
|
17591
|
-
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. 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 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
17591
|
+
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. 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. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
17592
17592
|
parameters: {
|
|
17593
17593
|
projectPath: PROJECT_PATH,
|
|
17594
17594
|
path: {
|
|
17595
17595
|
type: ["string", "array"],
|
|
17596
17596
|
items: { type: "string" },
|
|
17597
|
-
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once.
|
|
17597
|
+
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default test."
|
|
17598
17598
|
},
|
|
17599
17599
|
ignore: {
|
|
17600
17600
|
type: "array",
|
|
@@ -17908,7 +17908,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17908
17908
|
},
|
|
17909
17909
|
{
|
|
17910
17910
|
name: "editor_run",
|
|
17911
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
17911
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for, and the note then says what a runtime_* call naming no projectPath meets until it announces. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
17912
17912
|
parameters: {
|
|
17913
17913
|
projectPath: {
|
|
17914
17914
|
...PROJECT_PATH,
|
|
@@ -18124,7 +18124,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18124
18124
|
property: {
|
|
18125
18125
|
type: "string",
|
|
18126
18126
|
ops: ["property", "find"],
|
|
18127
|
-
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 packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that
|
|
18127
|
+
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 packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) 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. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control\'s theme is read where it is drawn: \'get_theme_stylebox("panel"):content_margin_top\' or \'get_theme_constant("separation")\'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. 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.'
|
|
18128
18128
|
},
|
|
18129
18129
|
depth: {
|
|
18130
18130
|
type: "integer",
|
|
@@ -18221,7 +18221,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18221
18221
|
property: {
|
|
18222
18222
|
type: "string",
|
|
18223
18223
|
ops: ["set"],
|
|
18224
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method
|
|
18224
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a reading method returned, as runtime_inspect property describes. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
18225
18225
|
},
|
|
18226
18226
|
value: {
|
|
18227
18227
|
ops: ["set"],
|
|
@@ -18231,12 +18231,18 @@ var init_tool_definitions = __esm(() => {
|
|
|
18231
18231
|
method: {
|
|
18232
18232
|
type: "string",
|
|
18233
18233
|
ops: ["call"],
|
|
18234
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method
|
|
18234
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a reading method returned, "get_viewport():gui_get_focus_owner", as runtime_inspect property describes.'
|
|
18235
18235
|
},
|
|
18236
18236
|
args: {
|
|
18237
18237
|
type: "array",
|
|
18238
18238
|
ops: ["call"],
|
|
18239
|
-
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, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. 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.`
|
|
18239
|
+
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, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. 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. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
18240
|
+
},
|
|
18241
|
+
properties: {
|
|
18242
|
+
type: ["boolean", "array"],
|
|
18243
|
+
items: { type: "string" },
|
|
18244
|
+
ops: ["call"],
|
|
18245
|
+
description: 'call: read the object the call answered with in the same answer, under result_properties: true for every variable its script declares, or a list of names, each a colon path walked from that object the way property paths are ("dead_allowed", "terms:0:name"). A name that is not there is listed under result_properties_missing with why, and a result that is not an object answers properties_note instead. Without it an object comes back as its class and id only.'
|
|
18240
18246
|
},
|
|
18241
18247
|
timeoutMs: {
|
|
18242
18248
|
type: "integer",
|
|
@@ -18318,7 +18324,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18318
18324
|
},
|
|
18319
18325
|
{
|
|
18320
18326
|
name: "runtime_input",
|
|
18321
|
-
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is:
|
|
18327
|
+
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
|
|
18322
18328
|
parameters: {
|
|
18323
18329
|
projectPath: RUNNING_PROJECT_PATH,
|
|
18324
18330
|
pid: RUNNING_PID,
|
|
@@ -18330,7 +18336,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18330
18336
|
says: {
|
|
18331
18337
|
type: "string",
|
|
18332
18338
|
ops: ["click"],
|
|
18333
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
18339
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when a control saying exactly the words is hidden, off the screen or covered, since it would stand in for that one; index presses it anyway. An empty field says its placeholder. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
18334
18340
|
},
|
|
18335
18341
|
action: {
|
|
18336
18342
|
type: "string",
|
|
@@ -18407,7 +18413,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18407
18413
|
requires: [],
|
|
18408
18414
|
operations: {
|
|
18409
18415
|
click: {
|
|
18410
|
-
summary:
|
|
18416
|
+
summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and link_pressed names the meta the label reports clicked. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
|
|
18411
18417
|
requires: []
|
|
18412
18418
|
},
|
|
18413
18419
|
choose: {
|
|
@@ -41895,10 +41901,13 @@ function runtimeVerdict(endpoint, after) {
|
|
|
41895
41901
|
return {
|
|
41896
41902
|
listening: false,
|
|
41897
41903
|
mayYetAnnounce: after.running,
|
|
41898
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
41904
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}. ${untilItAnnounces(after.servesAProject)}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
41899
41905
|
heldAt: null
|
|
41900
41906
|
};
|
|
41901
41907
|
}
|
|
41908
|
+
function untilItAnnounces(servesAProject) {
|
|
41909
|
+
return servesAProject ? "Until it does, a runtime_* call for it waits briefly and is then refused, and another project's game answers only a call whose projectPath names that project." : "Until it does, a runtime_* call naming no projectPath reaches whichever game is the only one announced, another project's included, so pass projectPath to be sure the answer is this game's.";
|
|
41910
|
+
}
|
|
41902
41911
|
function isOfType(value, kind) {
|
|
41903
41912
|
switch (kind) {
|
|
41904
41913
|
case "string":
|
|
@@ -42073,18 +42082,29 @@ function settingsNamedBy(op, args) {
|
|
|
42073
42082
|
return [];
|
|
42074
42083
|
}
|
|
42075
42084
|
}
|
|
42085
|
+
function foldedScriptErrors(errors) {
|
|
42086
|
+
const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED));
|
|
42087
|
+
const own = new Set(scriptErrors.map((error) => error.path));
|
|
42088
|
+
return {
|
|
42089
|
+
scriptErrors,
|
|
42090
|
+
failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path))
|
|
42091
|
+
};
|
|
42092
|
+
}
|
|
42093
|
+
function nonEmpty(lists) {
|
|
42094
|
+
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
42095
|
+
}
|
|
42076
42096
|
function scriptErrorsNote(errors) {
|
|
42097
|
+
const { scriptErrors: own, failedThroughDependency: dependents } = foldedScriptErrors(errors);
|
|
42077
42098
|
const causes = new Map;
|
|
42078
|
-
for (const error of
|
|
42079
|
-
if (!
|
|
42099
|
+
for (const error of own) {
|
|
42100
|
+
if (!causes.has(error.path)) {
|
|
42080
42101
|
causes.set(error.path, error);
|
|
42081
42102
|
}
|
|
42082
42103
|
}
|
|
42083
|
-
const dependents = [...new Set(errors.map((error) => error.path))].filter((path) => !causes.has(path));
|
|
42084
42104
|
const scripts = causes.size + dependents.length;
|
|
42085
42105
|
const named = [...causes.values()].map((error) => `${error.line === null ? error.path : `${error.path}:${error.line}`} ${error.message}${/[.!?]\)?$/.test(error.message) ? "" : "."}`).join(" ");
|
|
42086
42106
|
const following = dependents.length === 0 ? "" : causes.size === 0 ? `${dependents.join(", ")} ${dependents.length === 1 ? "depends" : "depend"} on a script that did not compile, and gdUnit4 named none with an error of its own: script_diagnostics on one of them finds it.` : `${dependents.length === 1 ? "One more" : `${dependents.length} more`} failed only because a script ${dependents.length === 1 ? "it depends" : "they depend"} on did: ${dependents.join(", ")}.`;
|
|
42087
|
-
const more =
|
|
42107
|
+
const more = own.length - causes.size;
|
|
42088
42108
|
const rest = more > 0 ? `${more === 1 ? "One more error" : `${more} more errors`} in ${causes.size === 1 ? "that script" : "those scripts"} ${more === 1 ? "is" : "are"} under scriptErrors.` : "";
|
|
42089
42109
|
return [
|
|
42090
42110
|
`No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.`,
|
|
@@ -42917,7 +42937,8 @@ class GodotServer {
|
|
|
42917
42937
|
...whichGame(args),
|
|
42918
42938
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
42919
42939
|
method: readString(args, "method") ?? "",
|
|
42920
|
-
args: readArray(args, "args") ?? []
|
|
42940
|
+
args: readArray(args, "args") ?? [],
|
|
42941
|
+
...args["properties"] === undefined ? {} : { properties: args["properties"] }
|
|
42921
42942
|
}, patience);
|
|
42922
42943
|
}
|
|
42923
42944
|
case "runtime_capture":
|
|
@@ -43711,9 +43732,9 @@ class GodotServer {
|
|
|
43711
43732
|
if (!paths.ok) {
|
|
43712
43733
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
43713
43734
|
}
|
|
43714
|
-
const
|
|
43715
|
-
const
|
|
43716
|
-
if (
|
|
43735
|
+
const missing = paths.value.filter((path) => !existsSync19(join24(project.value.path, path.slice("res://".length))));
|
|
43736
|
+
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
43737
|
+
if (asked.length === 0) {
|
|
43717
43738
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
43718
43739
|
return {
|
|
43719
43740
|
content: [
|
|
@@ -43830,7 +43851,10 @@ class GodotServer {
|
|
|
43830
43851
|
const hung = cutShort?.hung ?? false;
|
|
43831
43852
|
const scriptErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
|
|
43832
43853
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
43833
|
-
const
|
|
43854
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
43855
|
+
const nothingAt = missing.join(" and ");
|
|
43856
|
+
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
43857
|
+
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
43834
43858
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
43835
43859
|
const marker = readNonEmptyString(args, "printed");
|
|
43836
43860
|
const echoed = marker === undefined ? undefined : run.log.select({ severity: "info", sinceLastCall: false, contains: marker, limit: 200 });
|
|
@@ -43839,7 +43863,8 @@ class GodotServer {
|
|
|
43839
43863
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
43840
43864
|
};
|
|
43841
43865
|
if (report === null) {
|
|
43842
|
-
const
|
|
43866
|
+
const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).`;
|
|
43867
|
+
const note = missingNote === undefined ? said : `${said} ${missingNote}`;
|
|
43843
43868
|
return {
|
|
43844
43869
|
content: [
|
|
43845
43870
|
{ type: "text", text: note },
|
|
@@ -43853,7 +43878,8 @@ class GodotServer {
|
|
|
43853
43878
|
exitSignal: run.exitSignal ?? undefined,
|
|
43854
43879
|
hung,
|
|
43855
43880
|
...timedOut ? { timedOut, silentForMs } : {},
|
|
43856
|
-
...
|
|
43881
|
+
...missing.length > 0 ? { missing } : {},
|
|
43882
|
+
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
43857
43883
|
arguments: cmdArgs,
|
|
43858
43884
|
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
43859
43885
|
...printedLines,
|
|
@@ -43890,8 +43916,9 @@ class GodotServer {
|
|
|
43890
43916
|
});
|
|
43891
43917
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
43892
43918
|
return this.jsonTextResponse({
|
|
43893
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
43919
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
43894
43920
|
verdict,
|
|
43921
|
+
...missing.length > 0 ? { missing } : {},
|
|
43895
43922
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
43896
43923
|
exitCode,
|
|
43897
43924
|
exitSignal: run.exitSignal ?? undefined,
|
|
@@ -43908,7 +43935,10 @@ class GodotServer {
|
|
|
43908
43935
|
...printedLines,
|
|
43909
43936
|
savesNote,
|
|
43910
43937
|
...scanned,
|
|
43911
|
-
note:
|
|
43938
|
+
note: [
|
|
43939
|
+
missingNote,
|
|
43940
|
+
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
43941
|
+
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
43912
43942
|
suites: unclean.map((suite) => ({
|
|
43913
43943
|
name: suite.name,
|
|
43914
43944
|
path: suite.path,
|
|
@@ -44682,7 +44712,14 @@ class GodotServer {
|
|
|
44682
44712
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
44683
44713
|
const budgetMs = budget.ms;
|
|
44684
44714
|
if (!existsSync19(join24(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
44685
|
-
return runtimeVerdict(null, {
|
|
44715
|
+
return runtimeVerdict(null, {
|
|
44716
|
+
addon: false,
|
|
44717
|
+
budgetMs,
|
|
44718
|
+
heldAt: null,
|
|
44719
|
+
running: false,
|
|
44720
|
+
withArgs,
|
|
44721
|
+
servesAProject: this.ownProject !== null
|
|
44722
|
+
});
|
|
44686
44723
|
}
|
|
44687
44724
|
let asking = null;
|
|
44688
44725
|
let editorAskedAt = 0;
|
|
@@ -44734,6 +44771,7 @@ class GodotServer {
|
|
|
44734
44771
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
44735
44772
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
44736
44773
|
withArgs,
|
|
44774
|
+
servesAProject: this.ownProject !== null,
|
|
44737
44775
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
44738
44776
|
});
|
|
44739
44777
|
}
|
|
@@ -47978,7 +48016,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
47978
48016
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
47979
48017
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
47980
48018
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
47981
|
-
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method
|
|
48019
|
+
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method that only reads and walks into what it returned; an engine method takes literal arguments in the brackets, \`get_theme_constant("separation")\` |
|
|
47982
48020
|
| Press a button | \`runtime_input click\` with \`says\` and the words on it, which finds it and presses it in the same frame and says what was under the pointer. A \`find\` and then a click by the path it answered is two calls over a generated path, and a panel that rebuilds between them frees the button first; \`nodePath\` narrows where \`says\` looks, and several matches are refused with an \`index\` for each |
|
|
47983
48021
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows (no \`nodePath\` needed: without one the whole game is searched), then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
47984
48022
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -233,13 +233,17 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
233
233
|
var read_from: int = Time.get_ticks_usec()
|
|
234
234
|
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
235
235
|
var read_for: int = Time.get_ticks_usec() - read_from
|
|
236
|
-
|
|
236
|
+
var listed: Dictionary = {
|
|
237
237
|
"type": "method_result",
|
|
238
238
|
"path": node_path,
|
|
239
239
|
"method": method,
|
|
240
240
|
"result": _values.serialize(answered),
|
|
241
241
|
"elapsed_usec": read_for,
|
|
242
242
|
}
|
|
243
|
+
var asked_of_it: Variant = params.get("properties")
|
|
244
|
+
if _reads_the_result(asked_of_it):
|
|
245
|
+
listed.merge(_read_off_result(answered, asked_of_it))
|
|
246
|
+
return listed
|
|
243
247
|
var why: String = (
|
|
244
248
|
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
245
249
|
if Paths.can_read(reached["holder"], spelled)
|
|
@@ -258,10 +262,11 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
258
262
|
# The method a call ends on is the one being called whether or not it carries the brackets a
|
|
259
263
|
# step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
|
|
260
264
|
# the last pair.
|
|
265
|
+
var not_called: String = _not_a_method_to_call(holder, named, str(reached["called"]))
|
|
266
|
+
if not not_called.is_empty():
|
|
267
|
+
return {"type": "error", "message": not_called}
|
|
261
268
|
if not Paths.method_of(named).is_empty():
|
|
262
269
|
named = Paths.method_of(named)
|
|
263
|
-
if not holder.has_method(named):
|
|
264
|
-
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
265
270
|
|
|
266
271
|
# Checked before the call rather than left to it. `callv` raises inside the game when an
|
|
267
272
|
# argument cannot be converted, and an error raised in a game somebody is playing holds it at a
|
|
@@ -360,9 +365,98 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
360
365
|
answer["errors"] = caught.raised.slice(0, ERRORS_KEPT)
|
|
361
366
|
if caught.raised.size() > ERRORS_KEPT:
|
|
362
367
|
answer["errors_not_shown"] = caught.raised.size() - ERRORS_KEPT
|
|
368
|
+
var asked: Variant = params.get("properties")
|
|
369
|
+
if _reads_the_result(asked):
|
|
370
|
+
answer.merge(_read_off_result(result, asked))
|
|
363
371
|
return answer
|
|
364
372
|
|
|
365
373
|
|
|
374
|
+
## Why [param named], the last step of a call's method, names no method of [param holder] to call, or
|
|
375
|
+
## "" when it does. Brackets on it holding arguments are refused rather than read: the arguments are
|
|
376
|
+
## taken from args alone, so a call is never made with half of them in one place and half in the
|
|
377
|
+
## other, or with the bracketed ones quietly dropped.
|
|
378
|
+
static func _not_a_method_to_call(holder: Object, named: String, called: String) -> String:
|
|
379
|
+
var method: String = named
|
|
380
|
+
if not Paths.method_of(named).is_empty():
|
|
381
|
+
var bracketed: Dictionary = Paths.arguments_of(named)
|
|
382
|
+
var in_brackets: Array = bracketed.get("args", [])
|
|
383
|
+
if bracketed.has("message") or not in_brackets.is_empty():
|
|
384
|
+
return (
|
|
385
|
+
"%s:%s: the method a call ends on takes its arguments under args, not in its brackets"
|
|
386
|
+
% [called, named]
|
|
387
|
+
)
|
|
388
|
+
method = Paths.method_of(named)
|
|
389
|
+
return "" if holder.has_method(method) else "%s has no method %s" % [called, method]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## Whether a call's `properties` asks for its result to be read: true, or a list of names.
|
|
393
|
+
static func _reads_the_result(asked: Variant) -> bool:
|
|
394
|
+
return asked is Array or (asked is bool and asked)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
## What [param asked] reads off the object a call answered with: every variable its script declares
|
|
398
|
+
## when [param asked] is true, or the properties a list names, each a path walked from the object.
|
|
399
|
+
##
|
|
400
|
+
## In the same answer because an object handed back is otherwise only its class and an id, and
|
|
401
|
+
## reading one field of it meant finding the same object again by a path into the game's private
|
|
402
|
+
## state, which works only where the caller already knows a key. A name that is not there is listed
|
|
403
|
+
## rather than refusing the answer, since the call has run by then and its result is what was asked.
|
|
404
|
+
func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
|
|
405
|
+
if result is Object and not is_instance_valid(result):
|
|
406
|
+
return {
|
|
407
|
+
"properties_note": "the call answered an object that has since been freed, so nothing was read"
|
|
408
|
+
}
|
|
409
|
+
if not result is Object:
|
|
410
|
+
return {
|
|
411
|
+
"properties_note":
|
|
412
|
+
"the call answered %s, which has no properties to read" % type_string(typeof(result))
|
|
413
|
+
}
|
|
414
|
+
var object: Object = result
|
|
415
|
+
var wanted: Array[String] = []
|
|
416
|
+
if asked is Array:
|
|
417
|
+
for name: Variant in asked:
|
|
418
|
+
wanted.append(str(name))
|
|
419
|
+
else:
|
|
420
|
+
wanted = _script_variables(object)
|
|
421
|
+
var read: Dictionary = {}
|
|
422
|
+
var missing: Array[String] = []
|
|
423
|
+
for path: String in wanted:
|
|
424
|
+
var walked: Dictionary = _walk_from(object, path)
|
|
425
|
+
if walked.has("message"):
|
|
426
|
+
missing.append(str(walked["message"]))
|
|
427
|
+
else:
|
|
428
|
+
read[path] = _values.serialize(walked["value"])
|
|
429
|
+
var answered: Dictionary = {"result_properties": read}
|
|
430
|
+
if not missing.is_empty():
|
|
431
|
+
answered["result_properties_missing"] = missing
|
|
432
|
+
return answered
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
## The variables the script attached to [param object] declares, in the order it declares them.
|
|
436
|
+
static func _script_variables(object: Object) -> Array[String]:
|
|
437
|
+
var names: Array[String] = []
|
|
438
|
+
for entry: Dictionary in object.get_property_list():
|
|
439
|
+
var usage: int = entry.get("usage", 0)
|
|
440
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE != 0:
|
|
441
|
+
names.append(str(entry.get("name", "")))
|
|
442
|
+
return names
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
## What [param path] reads walked from [param object], as {"value"}, or {"message"} naming the step
|
|
446
|
+
## that is not there, by the same rules a path read from a node follows.
|
|
447
|
+
static func _walk_from(object: Object, path: String) -> Dictionary:
|
|
448
|
+
var holder: Variant = object
|
|
449
|
+
var called: String = "the result"
|
|
450
|
+
for named: String in Paths.steps_of(path):
|
|
451
|
+
if not (holder is Object or holder is Array or holder is Dictionary or Paths.packed(holder)):
|
|
452
|
+
return {"message": "%s holds no object to read %s off" % [called, named]}
|
|
453
|
+
if not Paths.can_read(holder, named):
|
|
454
|
+
return {"message": Paths.nothing_there(holder, named, called)}
|
|
455
|
+
holder = Paths.read_under(holder, named)
|
|
456
|
+
called = "%s:%s" % [called, named]
|
|
457
|
+
return {"value": holder}
|
|
458
|
+
|
|
459
|
+
|
|
366
460
|
## Why [param given] arguments are the wrong number for [param method], or "" when they are not.
|
|
367
461
|
##
|
|
368
462
|
## Asked before the call, because `callv` given the wrong number refuses inside the game and returns
|
|
@@ -25,6 +25,9 @@ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
|
25
25
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
26
26
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
27
27
|
|
|
28
|
+
## Whether this run has had the pointer note in full; the game process is the run.
|
|
29
|
+
static var _pointer_explained: bool = false
|
|
30
|
+
|
|
28
31
|
var _host: Node
|
|
29
32
|
var _values: Values
|
|
30
33
|
|
|
@@ -60,13 +63,21 @@ func _init(host: Node, values: Values) -> void:
|
|
|
60
63
|
## that would take the screen. What reads the injected event is right, tooltips included, since the
|
|
61
64
|
## engine places a tooltip from the motion that reached the control.
|
|
62
65
|
static func _pointer_noted(answer: Dictionary) -> Dictionary:
|
|
63
|
-
if OS.get_environment(DESKTOP_VARIABLE)
|
|
66
|
+
if OS.get_environment(DESKTOP_VARIABLE) == "":
|
|
67
|
+
return answer
|
|
68
|
+
# The fact on every answer and the explanation on the first of the run: a session sends dozens of
|
|
69
|
+
# pointer calls to one game, and the same four hundred characters on each was most of what came
|
|
70
|
+
# back.
|
|
71
|
+
answer["pointer_on_hidden_desktop"] = true
|
|
72
|
+
if not _pointer_explained:
|
|
73
|
+
_pointer_explained = true
|
|
64
74
|
answer["pointer_note"] = (
|
|
65
75
|
"This game is on gdharness's own desktop, where Windows refuses to say where the"
|
|
66
76
|
+ " pointer is, so get_mouse_position() on the root window, and get_global_mouse_position()"
|
|
67
77
|
+ " and get_local_mouse_position() on what is drawn there, read a meaningless position"
|
|
68
78
|
+ " rather than where this put it. What reads the event itself gets this position: the"
|
|
69
|
-
+ " control under the pointer, its hover and tooltip, and the game's input handlers."
|
|
79
|
+
+ " control under the pointer, its hover and tooltip, and the game's input handlers. Said"
|
|
80
|
+
+ " once a run; pointer_on_hidden_desktop marks every answer it applies to."
|
|
70
81
|
)
|
|
71
82
|
return answer
|
|
72
83
|
|
|
@@ -408,9 +419,27 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
408
419
|
if not unmoved.is_empty():
|
|
409
420
|
return {"type": "error", "message": unmoved}
|
|
410
421
|
|
|
422
|
+
# Words that are part of a rich label are pressed where they are drawn, since its middle can be on
|
|
423
|
+
# other words, a link among them. Said either way, so a press on the middle is never read as one
|
|
424
|
+
# on the words.
|
|
425
|
+
var aim: Vector2 = control.size * 0.5
|
|
426
|
+
var rich: RichTextLabel = control as RichTextLabel
|
|
427
|
+
if found != null and rich != null:
|
|
428
|
+
var placed: Dictionary = Targets.point_on_words(rich, wanted)
|
|
429
|
+
var found_here: Dictionary = found
|
|
430
|
+
if placed.has("point"):
|
|
431
|
+
aim = placed["point"]
|
|
432
|
+
found_here["pressedOn"] = "the words"
|
|
433
|
+
elif placed.has("unplaced"):
|
|
434
|
+
found_here["pressedOn"] = "the middle of the label"
|
|
435
|
+
found_here["note"] = (
|
|
436
|
+
"the words are part of a longer label and %s, so the press went to its middle"
|
|
437
|
+
% placed["unplaced"]
|
|
438
|
+
)
|
|
439
|
+
|
|
411
440
|
# In the control's own viewport first, which is the space its scrolling happens in.
|
|
412
441
|
var own: Viewport = control.get_viewport()
|
|
413
|
-
var local: Vector2 = control.get_global_transform_with_canvas() *
|
|
442
|
+
var local: Vector2 = control.get_global_transform_with_canvas() * aim
|
|
414
443
|
|
|
415
444
|
# A control out of sight inside a ScrollContainer is not out of reach, it is one scroll away,
|
|
416
445
|
# which is what a person does without thinking about it before they click. Refusing it
|
|
@@ -434,7 +463,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
434
463
|
% [node_path, "freed" if left == "freed" else "taken out of the tree"]
|
|
435
464
|
)
|
|
436
465
|
}
|
|
437
|
-
local = control.get_global_transform_with_canvas() *
|
|
466
|
+
local = control.get_global_transform_with_canvas() * aim
|
|
438
467
|
|
|
439
468
|
var reached: Dictionary = Screen.reach(own, local)
|
|
440
469
|
var viewport: Viewport = reached["viewport"]
|
|
@@ -472,12 +501,19 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
472
501
|
hovered_path = str(hovered.get_path())
|
|
473
502
|
var landed: bool = hovered == control or (hovered != null and control.is_ancestor_of(hovered))
|
|
474
503
|
|
|
504
|
+
# The link a rich label says was pressed, which is its own word for what the press reached.
|
|
505
|
+
var links: Array[String] = []
|
|
506
|
+
var heard: Callable = func(meta: Variant) -> void: links.append(str(meta))
|
|
507
|
+
if rich != null:
|
|
508
|
+
var _listening: int = rich.meta_clicked.connect(heard)
|
|
475
509
|
_press_button(viewport, _button(position, button, true, double))
|
|
476
510
|
await _host.get_tree().process_frame
|
|
477
511
|
_press_button(viewport, _button(position, button, false, false))
|
|
478
512
|
# The release is what a button acts on, and a queue_free it causes lands at the end of
|
|
479
513
|
# this frame; the frame passes so the answer describes the control as the click left it.
|
|
480
514
|
await _host.get_tree().process_frame
|
|
515
|
+
if rich != null and is_instance_valid(rich) and rich.meta_clicked.is_connected(heard):
|
|
516
|
+
rich.meta_clicked.disconnect(heard)
|
|
481
517
|
|
|
482
518
|
# What became of the control: still in the tree, taken out of it, or freed. A button that
|
|
483
519
|
# opened another screen is the second or the third, and the caller wants to hear that
|
|
@@ -497,6 +533,8 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
497
533
|
}
|
|
498
534
|
if found != null:
|
|
499
535
|
answer["found"] = found
|
|
536
|
+
if not links.is_empty():
|
|
537
|
+
answer["link_pressed"] = links.back()
|
|
500
538
|
return _pointer_noted(answer)
|
|
501
539
|
|
|
502
540
|
|