gdharness 1.3.36 → 1.3.38
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 +79 -28
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +20 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +6 -2
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +54 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +18 -59
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +57 -1
- package/build/godot/operations/dependencies.gd +58 -9
- package/build/keeper.js +5 -5
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -10508,9 +10508,24 @@ function parseJUnit(xml) {
|
|
|
10508
10508
|
};
|
|
10509
10509
|
}
|
|
10510
10510
|
function hookFailuresPrinted(printed) {
|
|
10511
|
+
let above = null;
|
|
10511
10512
|
return printed.flatMap((line) => {
|
|
10513
|
+
if (REPORT_HEADING.test(line)) {
|
|
10514
|
+
above = [];
|
|
10515
|
+
return [];
|
|
10516
|
+
}
|
|
10517
|
+
if (REPORT_END.test(line)) {
|
|
10518
|
+
above = null;
|
|
10519
|
+
return [];
|
|
10520
|
+
}
|
|
10512
10521
|
const found = HOOK_FAILURE.exec(line);
|
|
10513
|
-
|
|
10522
|
+
if (found === null) {
|
|
10523
|
+
above?.push(line.trim());
|
|
10524
|
+
return [];
|
|
10525
|
+
}
|
|
10526
|
+
const message = [...above ?? [], found[1] ?? ""].filter((part) => part !== "").join(" ");
|
|
10527
|
+
above = [];
|
|
10528
|
+
return [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message }];
|
|
10514
10529
|
});
|
|
10515
10530
|
}
|
|
10516
10531
|
function whyNoReport(printed, asked) {
|
|
@@ -10784,7 +10799,7 @@ function testProgressMessage(printed, elapsedMs) {
|
|
|
10784
10799
|
const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
|
|
10785
10800
|
return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
|
|
10786
10801
|
}
|
|
10787
|
-
var MalformedReportError, ENTITIES, ATTRIBUTE, IGNORED_ROW_LINE, suiteNameOf = (path) => (path.split("/").at(-1) ?? path).replace(/\.gd$/, ""), HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, 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.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, ORPHANS_LEFT, HOOKS_LEFT, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
|
|
10802
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, IGNORED_ROW_LINE, suiteNameOf = (path) => (path.split("/").at(-1) ?? path).replace(/\.gd$/, ""), HOOK_FAILURE, REPORT_HEADING, REPORT_END, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, 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.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, ORPHANS_LEFT, HOOKS_LEFT, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
|
|
10788
10803
|
var init_junit = __esm(() => {
|
|
10789
10804
|
MalformedReportError = class MalformedReportError extends Error {
|
|
10790
10805
|
constructor(message) {
|
|
@@ -10796,6 +10811,8 @@ var init_junit = __esm(() => {
|
|
|
10796
10811
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
10797
10812
|
IGNORED_ROW_LINE = /^gdharness ignored (res:\/\/.+?\.gd > [^:\s]+:\d+)\s*$/;
|
|
10798
10813
|
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
10814
|
+
REPORT_HEADING = /\bfinalize\(\)\s+Report:\s*$/;
|
|
10815
|
+
REPORT_END = /^\s*Statistics:/;
|
|
10799
10816
|
NOTHING_RAN = /no test cases found/i;
|
|
10800
10817
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
10801
10818
|
ESCAPE = String.fromCharCode(27);
|
|
@@ -11074,6 +11091,12 @@ function named(items) {
|
|
|
11074
11091
|
const shown = items.slice(0, MOST_NAMED).join(", ");
|
|
11075
11092
|
return items.length > MOST_NAMED ? `${shown} and ${String(items.length - MOST_NAMED)} more` : shown;
|
|
11076
11093
|
}
|
|
11094
|
+
function uncollectedSaid(found) {
|
|
11095
|
+
return found.map(({ path, cases }) => {
|
|
11096
|
+
const one = cases.length === 1;
|
|
11097
|
+
return `${path} declares ${named(cases)} and does not extend GdUnitTestSuite, directly or through what it extends, so gdUnit4 never runs ${one ? "it" : "them"}: extend GdUnitTestSuite, or take the test_ prefix off ${one ? "a function that is" : "functions that are"} no case`;
|
|
11098
|
+
});
|
|
11099
|
+
}
|
|
11077
11100
|
function judgeEngine(printed, handed, end) {
|
|
11078
11101
|
const faults = [];
|
|
11079
11102
|
const failing = new Set;
|
|
@@ -16218,7 +16241,7 @@ function annotationsOf(spec) {
|
|
|
16218
16241
|
openWorldHint: false
|
|
16219
16242
|
};
|
|
16220
16243
|
}
|
|
16221
|
-
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. An editor of the project that is running and has not reached this server is waited for rather than written around.", ALSO_SAVED = "A scene is saved as the engine holds it once the file has loaded, and one property can set others as it loads, as anchors_preset gives a centred Control both its grow directions, so a scene not last saved by an editor can gain, change or lose lines the call did not ask for, on any node the file held, the one it names among them: alsoSaved lists each as {node, property, was, saved}, was left out where the file set nothing and saved where it now sets nothing. What a set itself moved on its node is under alsoChanged rather than here, and a node the call adds
|
|
16244
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. Each file the written one refers to is named by its uid, as the editor names it, except a file the project's uid cache has no uid for yet (one made since the project was last scanned), which is named by path and listed under namedByPath with the call that scans it. An editor of the project that is running and has not reached this server is waited for rather than written around.", ALSO_SAVED = "A scene is saved as the engine holds it once the file has loaded, and one property can set others as it loads, as anchors_preset gives a centred Control both its grow directions, so a scene not last saved by an editor can gain, change or lose lines the call did not ask for, on any node the file held, the one it names among them: alsoSaved lists each as {node, property, was, saved}, was left out where the file set nothing and saved where it now sets nothing. What a set itself moved on its node is under alsoChanged rather than here, and neither a node the call adds nor the format lines (header uid, load_steps, unique_id) are listed. The scene loads as it did; only its text changed.", IMPORTED_FIRST = "importedFirst names the files whose import was behind them on disk, such as a picture changed since it was imported or one never imported, which were imported before the run through project_import reimport, so the run reads them as they are now; it is left out when every import was current. notImportedFirst lists, as project_import reimport does, any it could not import, and importNote says that the run reads those as they were. The import gives every script without a .uid sidecar one, as the editor's scan does, writing it beside the script in the project, and uidsWrittenFirst names those scripts. The files inside an addon are counted per addon rather than named, under importedInAddons and uidsWrittenInAddons. importTookMs is how long the import took; it is outside the run's own time limit, so a call that imported can take that much longer than the limit it was given.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', EXPORT_PLATFORMS, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
|
|
16222
16245
|
var init_tool_definitions = __esm(() => {
|
|
16223
16246
|
init_launch();
|
|
16224
16247
|
PROJECT_INFO_SECTIONS = [
|
|
@@ -16298,7 +16321,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16298
16321
|
property: { type: "string", description: "For property tracks: the property to animate." },
|
|
16299
16322
|
method: {
|
|
16300
16323
|
type: "string",
|
|
16301
|
-
description: "For method tracks: the method to call. The answer's methodFound says whether the target node has it; a method it lacks is still added, since it may be written next, and until it exists each call the track makes fails. A key whose args the method cannot take (too few or too many, or an object where it takes a value or the reverse) is written too, and answers argumentsFit false with misfit saying why. remove_track: only the keys calling this method."
|
|
16324
|
+
description: "For method tracks: the method to call. A method track holds every method key for its node, so a key calling another method joins the node's track, with existingTrack true. The answer's methodFound says whether the target node has it; a method it lacks is still added, while a nodePath that reaches no node is refused, since it may be written next, and until it exists each call the track makes fails. A key whose args the method cannot take (too few or too many, or an object where it takes a value or the reverse) is written too, and answers argumentsFit false with misfit saying why. remove_track: only the keys calling this method."
|
|
16302
16325
|
},
|
|
16303
16326
|
keyframes: {
|
|
16304
16327
|
type: "array",
|
|
@@ -16634,7 +16657,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16634
16657
|
direction: {
|
|
16635
16658
|
type: "string",
|
|
16636
16659
|
enum: ["forward", "reverse"],
|
|
16637
|
-
description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path, and a shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
16660
|
+
description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
16638
16661
|
},
|
|
16639
16662
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
16640
16663
|
includeBuiltin: {
|
|
@@ -16653,7 +16676,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16653
16676
|
name: "project_import",
|
|
16654
16677
|
title: "Asset imports",
|
|
16655
16678
|
description: "The import pipeline: what needs importing, how a resource is imported, reimports, UIDs, and the global class list the editor and the engine read.",
|
|
16656
|
-
answer: "reimport is answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. importedByTheScan names what the editor's scan imported first, and alsoImported, on a reimport of one resourcePath, the others it imported. wroteInProject names, by source, what an import wrote into the project itself rather than under .godot, such as the .translation a CSV gives each language, which version control sees and internationalization/locale/translations names. When nothing needed reimporting, the two lists are empty, a note says so, and there is no via. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, even when nothing needed reimporting; one resourcePath leaves it out. stillImporting is an editor that had not finished within timeoutMs.",
|
|
16679
|
+
answer: "reimport is answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. importedByTheScan names what the editor's scan imported first, and alsoImported, on a reimport of one resourcePath, the others it imported. wroteInProject names, by source, what an import wrote into the project itself rather than under .godot, such as the .translation a CSV gives each language, which version control sees and internationalization/locale/translations names. The import gives every script without a .uid sidecar one, as the editor's scan does, and uidsWritten names those scripts, counted per addon under uidsWrittenInAddons for the ones inside an addon. When nothing needed reimporting, the two lists are empty, a note says so, and there is no via. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, even when nothing needed reimporting; one resourcePath leaves it out. stillImporting is an editor that had not finished within timeoutMs.",
|
|
16657
16680
|
parameters: {
|
|
16658
16681
|
projectPath: PROJECT_PATH,
|
|
16659
16682
|
resourcePath: { ...RESOURCE_PATH, ops: ["status", "options", "set_options", "uid", "reimport"] },
|
|
@@ -16807,13 +16830,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
16807
16830
|
title: "Run tests",
|
|
16808
16831
|
effect: "additive",
|
|
16809
16832
|
description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and with script, line and function for the project script one points at, as editor_output's entries have. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
|
|
16810
|
-
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw (100 a case failed, 101 only warnings, 105 script errors it detected), and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. gdUnit4 does not fail a case on push_error or push_warning, so a case that printed one undeclared is in neither failed nor errors: it is named here, and the run is failed for it. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources, memory pages or a server's RIDs, which fail the run too, and the note names any script of the project Godot itself never frees and, on a run with engineArgs [\"--verbose\"], what the engine held. engineErrors counts every engine error the run printed but those a suite declared. orphans totals the orphan nodes gdUnit4 counted, and each suite that left some has its count, and a warning saying how to free them, under warnings, with left naming each case that left some (before or after for one of the suite's hooks, or the one value \"before or after\" where no case began and the two cannot be told apart) and the nodes, by class, name and script. suites lists only the suites with something to say (a case that failed, erred or was skipped, a hook that failed, orphans, or an error undeclared or declared and not printed), and suitesPassed counts the rest. Under classes, the classes each addon declares are counted under addedInAddons and changedInAddons rather than named, and a class the rebuild 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. A suite needs a window when it declares const GDHARNESS_NEEDS_WINDOW: bool = true, or calls scene_runner and declares nothing; a run holding one has a window, at the size project.godot gives its viewport and with no sound, and windowed is true and windowNote names those suites and any saying it runs headless that shared the window. On a machine with no display such a run is headless, windowed is false, and windowNote says so. " + IMPORTED_FIRST,
|
|
16833
|
+
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw (100 a case failed, 101 only warnings, 105 script errors it detected), and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. gdUnit4 does not fail a case on push_error or push_warning, so a case that printed one undeclared is in neither failed nor errors: it is named here, and the run is failed for it. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources, memory pages or a server's RIDs, which fail the run too, and the note names any script of the project Godot itself never frees and, on a run with engineArgs [\"--verbose\"], what the engine held. engineErrors counts every engine error the run printed but those a suite declared. orphans totals the orphan nodes gdUnit4 counted, and each suite that left some has its count, and a warning saying how to free them, under warnings, with left naming each case that left some (before or after for one of the suite's hooks, or the one value \"before or after\" where no case began and the two cannot be told apart) and the nodes, by class, name and script. suites lists only the suites with something to say (a case that failed, erred or was skipped, a hook that failed, orphans, or an error undeclared or declared and not printed), and suitesPassed counts the rest. A before() or after() that failed comes back under hookFailures with its suite, line and whole message; failures and errors count cases and leave it out, so the verdict names it as failed hooks, and the cases after a failed before() still run and are counted. Under classes, the classes each addon declares are counted under addedInAddons and changedInAddons rather than named, and a class the rebuild 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. A suite needs a window when it declares const GDHARNESS_NEEDS_WINDOW: bool = true, or calls scene_runner and declares nothing; a run holding one has a window, at the size project.godot gives its viewport and with no sound, and windowed is true and windowNote names those suites and any saying it runs headless that shared the window. On a machine with no display such a run is headless, windowed is false, and windowNote says so. " + IMPORTED_FIRST,
|
|
16811
16834
|
parameters: {
|
|
16812
16835
|
projectPath: PROJECT_PATH,
|
|
16813
16836
|
path: {
|
|
16814
16837
|
type: ["string", "array"],
|
|
16815
16838
|
items: { type: "string" },
|
|
16816
|
-
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. A script named that declares no test_ function at the top level is refused the same way. Default tests and test, those the project has, which is where gdharness test looks."
|
|
16839
|
+
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. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. Default tests and test, those the project has, which is where gdharness test looks."
|
|
16817
16840
|
},
|
|
16818
16841
|
ignore: {
|
|
16819
16842
|
type: "array",
|
|
@@ -17320,7 +17343,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17320
17343
|
operations: {
|
|
17321
17344
|
structure: {
|
|
17322
17345
|
effect: "read",
|
|
17323
|
-
summary: "
|
|
17346
|
+
summary: `functions (each parameter's default the source text after its =, so = null reads "null", and null for a parameter with none), variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies. A member or inner class with a ## documentation comment directly above it carries the comment's text as doc, and the script's own, the ## block above class_name or extends or one followed by a blank line before any member, comes back as class_doc`,
|
|
17324
17347
|
requires: []
|
|
17325
17348
|
},
|
|
17326
17349
|
symbols: {
|
|
@@ -17663,7 +17686,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17663
17686
|
name: "editor_output",
|
|
17664
17687
|
title: "Game and editor output",
|
|
17665
17688
|
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. A run answers with counts and a verdict: clean is true when no error was reported, and warnings, counted beside it, do not make it false. 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 reported a game as playing fifteen seconds after its process was 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, where the output worth reading usually is, since the class cache, the plugins and the language server speak before any harness connects. It answers counts with no clean, since an editor neither passes nor fails, the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
|
|
17666
|
-
answer: "An entry with lines under its headline has them as detail, and, when one of them names a project script, script, line and function say where: the at: line when it names one, otherwise the innermost frame of the backtrace that does. A push_error or push_warning has the engine's own push_error as its at: line, so its script and line are the game's call that raised it. Each entry's source says where it was read: transcript for a line of the run's transcript file, which holds both its streams and is how every run with one is read; stdout or stderr for a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note; debugger for what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on. A printerr line is info: it prints to stderr, and the engine reports no error for it. For op editor with no editor connected, connected is false, editorRunning says whether the editor whose console it is still runs, and editorNote whether this server closed it or it went without a close, as a crash does; the next editor gdharness opens on the project starts a new console.",
|
|
17689
|
+
answer: "An entry with lines under its headline has them as detail, and, when one of them names a project script, script, line and function say where: the at: line when it names one, otherwise the innermost frame of the backtrace that does. A push_error or push_warning has the engine's own push_error as its at: line, so its script and line are the game's call that raised it. Each entry's source says where it was read: transcript for a line of the run's transcript file, which holds both its streams and is how every run with one is read; stdout or stderr for a line of that stream of a run with no transcript, such as an editor-played run picked up after a reconnect whose first server left no note; debugger for what the editor's debug adapter reported and the game never printed, such as the script error a game stopped on. A printerr line is info: it prints to stderr, and the engine reports no error for it. For op editor with no editor connected, connected is false, editorRunning says whether the editor whose console it is still runs, and editorNote whether this server closed it or it went without a close, as a crash does; the next editor gdharness opens on the project starts a new console. With none connected and none ever opened here on the project, there is no console, and the refusal says so.",
|
|
17667
17690
|
parameters: {
|
|
17668
17691
|
projectPath: {
|
|
17669
17692
|
type: "string",
|
|
@@ -18054,7 +18077,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18054
18077
|
name: "runtime_input",
|
|
18055
18078
|
title: "Send input to the game",
|
|
18056
18079
|
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, mouse motion or drag. All of it works headless, where the viewport is the project's window size 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. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
18057
|
-
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. A button answers pressed_signal, whether its pressed signal fired during the click. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed 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. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. key, mouse_click, click, joypad_button and joypad_motion answer actions, the InputMap actions the event also presses; for a motion, those its direction reaches past their deadzone, and none for one inside it. mouse_click, mouse_motion and mouse_drag answer hovered, the control under the pointer once the events are handled, or null over none; the other events are aimed at nothing. mouse_click with a press, and mouse_drag, answer press_went_to, the path of what the press went to (a control, or a window embedded over the point), read before it, or null when nothing there takes the pointer; given nodePath naming a Control, landed says whether that is the control or inside it. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed, and one on a control with something drawn over it names that, a window stacked above the one the control is in among them. An action, a key, a joypad_button or a joypad_motion answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button, and a stick or dpad pressing ui_down moves a focused list's selection, or the focus, and never reaches the game's handler. Where the press moved the focus, focused_after names the Control holding it once the event was handled, or null when nothing does. A mouse_click, mouse_motion or mouse_drag given nodePath answers it under path, its points in the pixels of the window they went to, and that window under window when it is not the game's own. One with a point outside the viewport it went to says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
|
|
18080
|
+
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. A button answers pressed_signal, whether its pressed signal fired during the click. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed 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. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. key, mouse_click, click, joypad_button and joypad_motion answer actions, the InputMap actions the event also presses; for a motion, those its direction reaches past their deadzone, and none for one inside it. mouse_click, mouse_motion and mouse_drag answer hovered, the control under the pointer once the events are handled, or null over none; the other events are aimed at nothing. mouse_click with a press, and mouse_drag, answer press_went_to, the path of what the press went to (a control, or a window embedded over the point), read before it, or null when nothing there takes the pointer; given nodePath naming a Control, landed says whether that is the control or inside it. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed, and one on a control with something drawn over it names that, a window stacked above the one the control is in among them. An action, a key, a joypad_button or a joypad_motion answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button, and a stick or dpad pressing ui_down moves a focused list's selection, or the focus, and never reaches the game's handler. Where the press moved the focus, focused_after names the Control holding it once the event was handled, or null when nothing does. A key that a focused LineEdit or TextEdit took as typing names that field under typed_into, with what it holds under holds, since a field being edited keeps the key from the game's _unhandled_input. editing_ended true, on a key or on text, is a LineEdit that stopped being edited, as Enter does by submitting it: it keeps the focus, and nothing typed into it lands until it is clicked again. text with the focus on a control that is no LineEdit or TextEdit answers holds null and a note saying so, since a focused button takes a space or Enter as a press. A mouse_click, mouse_motion or mouse_drag given nodePath answers it under path, its points in the pixels of the window they went to, and that window under window when it is not the game's own. One with a point outside the viewport it went to says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
|
|
18058
18081
|
parameters: {
|
|
18059
18082
|
projectPath: RUNNING_PROJECT_PATH,
|
|
18060
18083
|
pid: RUNNING_PID,
|
|
@@ -18675,7 +18698,8 @@ class ProjectScripts {
|
|
|
18675
18698
|
return this.searched(roots).flatMap((path) => this.suite(path) ?? []);
|
|
18676
18699
|
}
|
|
18677
18700
|
uncollectedUnder(roots) {
|
|
18678
|
-
|
|
18701
|
+
const named = roots.filter((root) => root.endsWith(".gd") && this.sources.has(root));
|
|
18702
|
+
return [...new Set([...named, ...this.searched(roots)])].flatMap((path) => {
|
|
18679
18703
|
const cases = this.script(path)?.funcs.filter((name) => name.startsWith(CASE_PREFIX)) ?? [];
|
|
18680
18704
|
return cases.length === 0 || this.suite(path) !== null ? [] : [{ path, cases: [...cases].sort() }];
|
|
18681
18705
|
});
|
|
@@ -49966,9 +49990,13 @@ function failedVerdict(report) {
|
|
|
49966
49990
|
if (report === null) {
|
|
49967
49991
|
return "failures";
|
|
49968
49992
|
}
|
|
49969
|
-
const
|
|
49970
|
-
|
|
49971
|
-
|
|
49993
|
+
const parts = [
|
|
49994
|
+
report.failures > 0 ? "failures" : undefined,
|
|
49995
|
+
report.errors > 0 ? "errors" : undefined,
|
|
49996
|
+
report.hookFailures + report.hookErrors > 0 ? "failed hooks" : undefined
|
|
49997
|
+
].filter((part) => part !== undefined);
|
|
49998
|
+
const last = parts.pop();
|
|
49999
|
+
return last === undefined ? "failures" : parts.length === 0 ? last : `${parts.join(", ")} and ${last}`;
|
|
49972
50000
|
}
|
|
49973
50001
|
function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
|
|
49974
50002
|
const quiet = Math.round(silentForMs);
|
|
@@ -51904,8 +51932,18 @@ class GodotServer {
|
|
|
51904
51932
|
if (!contained.ok) {
|
|
51905
51933
|
return contained.response;
|
|
51906
51934
|
}
|
|
51935
|
+
const unminted = scriptsWithoutUid(project.value.path);
|
|
51907
51936
|
const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
51908
|
-
|
|
51937
|
+
if (!outcome.ok) {
|
|
51938
|
+
return outcome.response;
|
|
51939
|
+
}
|
|
51940
|
+
const stillUnminted = scriptsWithoutUid(project.value.path);
|
|
51941
|
+
const uids = withAddonPathsCounted(unminted.filter((script) => !stillUnminted.includes(script)).map((script) => `res://${script}`));
|
|
51942
|
+
return this.jsonTextResponse({
|
|
51943
|
+
...outcome.payload,
|
|
51944
|
+
...uids.own.length === 0 ? {} : { uidsWritten: uids.own },
|
|
51945
|
+
...Object.keys(uids.inAddons).length === 0 ? {} : { uidsWrittenInAddons: uids.inAddons }
|
|
51946
|
+
});
|
|
51909
51947
|
}
|
|
51910
51948
|
editorHasOpen(projectPath) {
|
|
51911
51949
|
const open = this.godotBridge.getStatus().projectPath;
|
|
@@ -52635,9 +52673,10 @@ ${platform}`;
|
|
|
52635
52673
|
}
|
|
52636
52674
|
const missing = paths.value.filter((path) => !existsSync34(join43(project.value.path, path.slice("res://".length))));
|
|
52637
52675
|
const projectScripts = new ProjectScripts(project.value.path);
|
|
52638
|
-
const
|
|
52676
|
+
const present = paths.value.filter((path) => !missing.includes(path));
|
|
52677
|
+
const asked = projectScripts.searchedAsTold(present);
|
|
52639
52678
|
const unusable = gdUnit4Unusable(project.value.path);
|
|
52640
|
-
if (asked.length === 0) {
|
|
52679
|
+
if (asked.length === 0 && missing.length > 0) {
|
|
52641
52680
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
52642
52681
|
const andNoRunner = unusable === null ? "" : `, and ${unusable.reason}`;
|
|
52643
52682
|
return {
|
|
@@ -52669,10 +52708,25 @@ ${platform}`;
|
|
|
52669
52708
|
const one = caseless.length === 1;
|
|
52670
52709
|
return this.createErrorResponse(`${caseless.join(" and ")} ${one ? "declares" : "declare"} no case: gdUnit4 runs the functions named test_ at the top level of a script that extends GdUnitTestSuite, and ${one ? "it has" : "they have"} none.`);
|
|
52671
52710
|
}
|
|
52711
|
+
const uncollected = projectScripts.uncollectedUnder(present);
|
|
52712
|
+
const suiteless = present.filter((path) => !path.endsWith(".gd") && projectScripts.suitesUnder([path]).length === 0);
|
|
52713
|
+
const namedNoSuite = uncollected.filter((one) => present.includes(one.path));
|
|
52714
|
+
if (suiteless.length > 0 || namedNoSuite.length > 0) {
|
|
52715
|
+
const one = suiteless.length === 1;
|
|
52716
|
+
const under = (path) => suiteless.some((folder) => path.startsWith(`${folder.replace(/\/+$/, "")}/`));
|
|
52717
|
+
const holding = uncollected.filter((script) => namedNoSuite.includes(script) || under(script.path));
|
|
52718
|
+
const keptOut = projectScripts.notSearched.map((folder) => `res://${folder.replace(/^res:\/\//, "")}`).filter((folder) => under(folder) && projectScripts.suitesUnder([folder]).length > 0);
|
|
52719
|
+
const keptOutSaid = keptOut.length === 0 ? "no script there does" : `the only ones there are under ${named(keptOut)}, which ${NOT_SEARCHED_SETTING} keeps out of every search: name ${keptOut.length === 1 ? "it" : "them"} as path to run ${keptOut.length === 1 ? "its" : "their"} suites`;
|
|
52720
|
+
return this.createErrorResponse([
|
|
52721
|
+
suiteless.length === 0 ? undefined : `${named(suiteless)} ${one ? "holds" : "hold"} no gdUnit4 suite: gdUnit4 runs the functions named test_ at the top level of a script that extends GdUnitTestSuite, directly or through what it extends, and ${keptOutSaid}.`,
|
|
52722
|
+
...uncollectedSaid(holding).map((sentence) => `${sentence}.`)
|
|
52723
|
+
].filter((part) => part !== undefined).join(" "));
|
|
52724
|
+
}
|
|
52672
52725
|
const selection = selectTests(project.value.path, asked, args["tests"]);
|
|
52673
52726
|
if (!selection.ok) {
|
|
52674
52727
|
return this.createErrorResponse(selection.reason);
|
|
52675
52728
|
}
|
|
52729
|
+
const notSuites = selection.value === null ? uncollectedSaid(uncollected) : [];
|
|
52676
52730
|
const leftOut = new Set(readStringArray(args, "ignore") ?? []);
|
|
52677
52731
|
const needs = windowNeedsOf(projectScripts, selection.value?.suites ?? asked);
|
|
52678
52732
|
const needing = needs.needing.filter((path) => !leftOut.has(basename13(path, ".gd")));
|
|
@@ -53015,9 +53069,9 @@ ${error.text}`;
|
|
|
53015
53069
|
...new Set((selection.value?.selected ?? []).filter((one) => !report.suites.some((suite) => (suite.path === one.suite || suite.name === basename13(one.suite, ".gd")) && suite.cases.some((ran) => ran.name === one.test || new RegExp(`^${one.test}\\W`).test(ran.name)))).map((one) => `${basename13(one.suite, ".gd")}:${one.test}`))
|
|
53016
53070
|
];
|
|
53017
53071
|
return this.jsonTextResponse({
|
|
53018
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0,
|
|
53072
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && notSuites.length === 0,
|
|
53019
53073
|
verdict: [
|
|
53020
|
-
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length > 0 ? undefined : verdict,
|
|
53074
|
+
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + notSuites.length > 0 ? undefined : verdict,
|
|
53021
53075
|
...[false, true].map((warning) => {
|
|
53022
53076
|
const count = undeclaredPrinted.filter((error) => error.warning === warning).length;
|
|
53023
53077
|
const word = warning ? "warning" : "error";
|
|
@@ -53029,9 +53083,11 @@ ${error.text}`;
|
|
|
53029
53083
|
return count > 0 ? `${count === 1 ? `one declared ${word}` : `${String(count)} declared ${word}s`} never printed` : undefined;
|
|
53030
53084
|
}),
|
|
53031
53085
|
countedLeaks.length > 0 ? "the engine exited holding what it should have freed" : undefined,
|
|
53032
|
-
testsNotRun.length > 0 ? `not run: ${testsNotRun.join(", ")}` : undefined
|
|
53086
|
+
testsNotRun.length > 0 ? `not run: ${testsNotRun.join(", ")}` : undefined,
|
|
53087
|
+
notSuites.length > 0 ? `${notSuites.length === 1 ? "one script declares" : `${String(notSuites.length)} scripts declare`} cases gdUnit4 never runs` : undefined
|
|
53033
53088
|
].filter((part) => part !== undefined).join("; "),
|
|
53034
53089
|
...missing.length > 0 ? { missing } : {},
|
|
53090
|
+
...notSuites.length > 0 ? { notSuites } : {},
|
|
53035
53091
|
...testsNotRun.length > 0 ? { testsNotRun } : {},
|
|
53036
53092
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
53037
53093
|
exitCode,
|
|
@@ -53064,6 +53120,7 @@ ${error.text}`;
|
|
|
53064
53120
|
note: [
|
|
53065
53121
|
missingNote,
|
|
53066
53122
|
testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
|
|
53123
|
+
notSuites.length > 0 ? "What notSuites names declares cases gdUnit4 never runs, so the run is not called passed, as gdharness test judges it." : undefined,
|
|
53067
53124
|
briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
|
|
53068
53125
|
notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
|
|
53069
53126
|
unmatched.length > 0 ? `${unmatched.join(", ")} ${unmatched.length === 1 ? "names" : "name"} no suite, case or row under the paths run, so ${unmatched.length === 1 ? "it" : "they"} left nothing out.` : undefined,
|
|
@@ -55722,7 +55779,7 @@ ${error.text}`;
|
|
|
55722
55779
|
}
|
|
55723
55780
|
const noted = projectPath === undefined || greeted || launched !== null ? null : readEditorLogNote(projectPath);
|
|
55724
55781
|
const editorPid = greeted ? status.editorPid ?? null : launched ?? noted?.pid ?? null;
|
|
55725
|
-
const nothing = () => this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
55782
|
+
const nothing = () => this.createErrorResponse(projectPath !== undefined && !greeted && launched === null ? `No editor is connected, and no editor gdharness opened on ${projectPath} left a console behind, so there is none to read.` : "No editor is connected, so there is no console to read.", [
|
|
55726
55783
|
"editor_status says whether one is connected and whether it may yet be",
|
|
55727
55784
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
55728
55785
|
]);
|
|
@@ -58881,12 +58938,6 @@ function searchedRoots(options) {
|
|
|
58881
58938
|
return existsSync23(directory) && statSync14(directory).isDirectory();
|
|
58882
58939
|
});
|
|
58883
58940
|
}
|
|
58884
|
-
function uncollectedSaid(found) {
|
|
58885
|
-
return found.map(({ path, cases }) => {
|
|
58886
|
-
const one = cases.length === 1;
|
|
58887
|
-
return `${path} declares ${named(cases)} and does not extend GdUnitTestSuite, directly or through what it extends, so gdUnit4 never runs ${one ? "it" : "them"}: extend GdUnitTestSuite, or take the test_ prefix off ${one ? "a function that is" : "functions that are"} no case`;
|
|
58888
|
-
});
|
|
58889
|
-
}
|
|
58890
58941
|
function selectedSuites(scripts, options) {
|
|
58891
58942
|
const roots = rootsOf(options);
|
|
58892
58943
|
if (options.selected.length === 0) {
|
|
@@ -296,6 +296,7 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
296
296
|
if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
|
|
297
297
|
passed.append({"name": "source", "type": TYPE_OBJECT, "class_name": source.get_class()})
|
|
298
298
|
passed = passed.slice(0, maxi(passed.size() - callable.get_unbound_arguments_count(), 0))
|
|
299
|
+
var from_signal: int = passed.size()
|
|
299
300
|
for bound: Variant in callable.get_bound_arguments():
|
|
300
301
|
var bound_class: String = ""
|
|
301
302
|
if bound is Object:
|
|
@@ -310,7 +311,25 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
310
311
|
return "%s passes %s, and %s needs %d%s" % [signal_name, _count(passed.size()), method, needed, FAILS]
|
|
311
312
|
if passed.size() > parameters.size():
|
|
312
313
|
var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
|
|
313
|
-
|
|
314
|
+
# Said with the misfit, since unbinds is what a connection to a method wanting less is for, and
|
|
315
|
+
# only when the extra arguments are the signal's: a bound one is not dropped by unbinding.
|
|
316
|
+
var extra: int = passed.size() - parameters.size()
|
|
317
|
+
var remedy: String = (
|
|
318
|
+
(
|
|
319
|
+
" Connected with unbinds %d, the last %s the signal passes %s left out."
|
|
320
|
+
% [
|
|
321
|
+
extra + callable.get_unbound_arguments_count(),
|
|
322
|
+
"argument" if extra == 1 else "%d arguments" % extra,
|
|
323
|
+
"is" if extra == 1 else "are",
|
|
324
|
+
]
|
|
325
|
+
)
|
|
326
|
+
if extra <= from_signal
|
|
327
|
+
else ""
|
|
328
|
+
)
|
|
329
|
+
return (
|
|
330
|
+
"%s passes %s, and %s takes %s%s%s"
|
|
331
|
+
% [signal_name, _count(passed.size()), method, most, FAILS, remedy]
|
|
332
|
+
)
|
|
314
333
|
for at: int in passed.size():
|
|
315
334
|
var given: Dictionary = passed[at]
|
|
316
335
|
var parameter: Dictionary = parameters[at]
|
|
@@ -16,6 +16,8 @@ const GdhItems = preload("runtime_items.gd")
|
|
|
16
16
|
const GdhScreen = preload("runtime_screen.gd")
|
|
17
17
|
const GdhTargets = preload("runtime_targets.gd")
|
|
18
18
|
const GdhMouse = preload("runtime_mouse.gd")
|
|
19
|
+
const GdhTyping = preload("runtime_typing.gd")
|
|
20
|
+
const GdhSays = preload("runtime_says.gd")
|
|
19
21
|
|
|
20
22
|
## Set, to the desktop's name, for a process gdharness started on a desktop of its own and for
|
|
21
23
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
@@ -276,6 +278,8 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
276
278
|
event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
|
|
277
279
|
|
|
278
280
|
var focused: String = GdhValues.focus_path(_host)
|
|
281
|
+
var field: Control = GdhTyping.focus_owner(_host.get_tree().root)
|
|
282
|
+
var before: Dictionary = GdhTyping.field_before(field)
|
|
279
283
|
Input.parse_input_event(event)
|
|
280
284
|
var lasted: Variant = null
|
|
281
285
|
if whole:
|
|
@@ -300,7 +304,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
300
304
|
"pressed": held and not whole,
|
|
301
305
|
"whole": whole,
|
|
302
306
|
}
|
|
303
|
-
return GdhValues.focus_noted(answer, focused, _host)
|
|
307
|
+
return GdhTyping.field_noted(GdhValues.focus_noted(answer, focused, _host), field, before)
|
|
304
308
|
|
|
305
309
|
|
|
306
310
|
## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
|
|
@@ -601,7 +605,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
601
605
|
item_scrolled = placed_item["scrolled"]
|
|
602
606
|
var rich: RichTextLabel = control as RichTextLabel
|
|
603
607
|
if found != null and rich != null and named_point == null:
|
|
604
|
-
var placed: Dictionary =
|
|
608
|
+
var placed: Dictionary = GdhSays.point_on_words(rich, wanted)
|
|
605
609
|
var found_here: Dictionary = found
|
|
606
610
|
if placed.has("point"):
|
|
607
611
|
aim = placed["point"]
|
|
@@ -170,3 +170,57 @@ static func widened(wanted: String) -> String:
|
|
|
170
170
|
## an n, so the two characters mean the break to everybody who writes them.
|
|
171
171
|
static func as_said(wanted: String) -> String:
|
|
172
172
|
return spaced(wanted.replace("\\n", "\n"))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Where [param label] draws the words [param wanted] matched in it, in the label's own space, when
|
|
176
|
+
## they are part of its text: {"point"} on their middle, {"unplaced"} saying why there is none, or
|
|
177
|
+
## {} when the words are the whole text, whose middle is the label's.
|
|
178
|
+
##
|
|
179
|
+
## A line of a dispatch names two people, each a link to their page, and a press on the label's
|
|
180
|
+
## middle landed on the second name when the first was asked for, so the game opened the wrong page.
|
|
181
|
+
## Godot answers which line a character is on, where that line is and how wide it is drawn, and
|
|
182
|
+
## nothing narrower, so the place along the line is the font's measure of the text before the
|
|
183
|
+
## words. A line in another font or holding an image measures off from what is drawn, which a wide
|
|
184
|
+
## line with nothing between the words and its start hardly does.
|
|
185
|
+
static func point_on_words(label: RichTextLabel, wanted: String) -> Dictionary:
|
|
186
|
+
var text: String = spaced(GdhWords.said_by(label))
|
|
187
|
+
var start: int = -1
|
|
188
|
+
var length: int = 0
|
|
189
|
+
for words: String in alternatives(wanted):
|
|
190
|
+
if not is_plain(words):
|
|
191
|
+
continue
|
|
192
|
+
var at: int = text.findn(words)
|
|
193
|
+
if at >= 0 and (start < 0 or at < start):
|
|
194
|
+
start = at
|
|
195
|
+
length = words.length()
|
|
196
|
+
if start < 0:
|
|
197
|
+
return {"unplaced": "the words were matched as a pattern, which names no one place in the text"}
|
|
198
|
+
if text.strip_edges().length() == length:
|
|
199
|
+
return {}
|
|
200
|
+
var line: int = label.get_character_line(start)
|
|
201
|
+
var shown: Vector2i = label.get_line_range(line)
|
|
202
|
+
if line < 0 or shown.x > start:
|
|
203
|
+
return {"unplaced": "the label could not say which line the words are on"}
|
|
204
|
+
var on_line: int = mini(length, shown.y - start) if shown.y > start else length
|
|
205
|
+
var font: Font = label.get_theme_font("normal_font")
|
|
206
|
+
var font_size: int = label.get_theme_font_size("normal_font_size")
|
|
207
|
+
var before: float = _width(font, font_size, text.substr(shown.x, start - shown.x))
|
|
208
|
+
var across: float = _width(font, font_size, text.substr(start, on_line))
|
|
209
|
+
var frame: StyleBox = label.get_theme_stylebox("normal")
|
|
210
|
+
var left: float = frame.get_margin(SIDE_LEFT)
|
|
211
|
+
var room: float = label.size.x - left - frame.get_margin(SIDE_RIGHT)
|
|
212
|
+
var spare: float = maxf(room - label.get_line_width(line), 0.0)
|
|
213
|
+
var aligned: float = 0.0
|
|
214
|
+
if label.horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER:
|
|
215
|
+
aligned = spare * 0.5
|
|
216
|
+
elif label.horizontal_alignment == HORIZONTAL_ALIGNMENT_RIGHT:
|
|
217
|
+
aligned = spare
|
|
218
|
+
var scrolled: float = label.get_v_scroll_bar().value if label.scroll_active else 0.0
|
|
219
|
+
var x: float = clampf(left + aligned + before + across * 0.5, left, left + room)
|
|
220
|
+
var top: float = frame.get_margin(SIDE_TOP) + label.get_line_offset(line) - scrolled
|
|
221
|
+
return {"point": Vector2(x, top + label.get_line_height(line) * 0.5)}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## How wide [param font] at [param font_size] draws [param words] on one line.
|
|
225
|
+
static func _width(font: Font, font_size: int, words: String) -> float:
|
|
226
|
+
return font.get_string_size(words, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
@@ -12,6 +12,8 @@ const GdhWords = preload("runtime_words.gd")
|
|
|
12
12
|
|
|
13
13
|
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
14
14
|
const LISTED_CANDIDATES: int = 10
|
|
15
|
+
## How many hidden controls saying the words a refusal names.
|
|
16
|
+
const NAMED_HIDDEN: int = 3
|
|
15
17
|
|
|
16
18
|
## Node.ProcessMode as its names, for a refusal to say which one is in force.
|
|
17
19
|
const PROCESS_MODES: Dictionary[int, String] = {
|
|
@@ -63,7 +65,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
63
65
|
var exact: Array[bool] = []
|
|
64
66
|
# Whether the words are only inside longer words there, "out" in "Scout".
|
|
65
67
|
var inside: Array[bool] = []
|
|
66
|
-
var hidden:
|
|
68
|
+
var hidden: Array[Node] = []
|
|
67
69
|
# Controls saying exactly the words that a click cannot reach, kept to say what a match on screen
|
|
68
70
|
# that only says them as part of more stands in for.
|
|
69
71
|
var exact_out_of_reach: Array[Node] = []
|
|
@@ -77,7 +79,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
77
79
|
var node: Node = pending.pop_front()
|
|
78
80
|
if node is Control and GdhSays.says(node, wanted):
|
|
79
81
|
if not GdhQueries.shown(node):
|
|
80
|
-
hidden
|
|
82
|
+
hidden.append(node)
|
|
81
83
|
if _says_exactly(GdhWords.said_by(node), wanted):
|
|
82
84
|
exact_out_of_reach.append(node)
|
|
83
85
|
out_of_reach_as.append("hidden")
|
|
@@ -451,10 +453,21 @@ static func _offered_note(from: Node, drawn: Array[Node], areas: Array[Rect2]) -
|
|
|
451
453
|
return "" if words.is_empty() else "; the buttons on screen say %s" % words
|
|
452
454
|
|
|
453
455
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
+
## What to add to a refusal that found nothing on screen, when words matched hidden controls: how
|
|
457
|
+
## many, and the first few by path with what hides each. A count alone left a caller with nothing to
|
|
458
|
+
## show, since the control saying the words is rarely the node a game hides.
|
|
459
|
+
static func _hidden_note(hidden: Array[Node]) -> String:
|
|
460
|
+
if hidden.is_empty():
|
|
456
461
|
return ""
|
|
457
|
-
|
|
462
|
+
var named: Array[String] = []
|
|
463
|
+
for node: Node in hidden.slice(0, NAMED_HIDDEN):
|
|
464
|
+
named.append("%s%s" % [node.get_path(), why_hidden(node)])
|
|
465
|
+
if hidden.size() == 1:
|
|
466
|
+
return "; 1 hidden control says it, %s" % named[0]
|
|
467
|
+
return (
|
|
468
|
+
"; %d hidden controls say it, among them %s and %s"
|
|
469
|
+
% [hidden.size(), ", ".join(named.slice(0, -1)), named[-1]]
|
|
470
|
+
)
|
|
458
471
|
|
|
459
472
|
|
|
460
473
|
## What to add to a refusal that found nothing on screen, when words matched controls a click cannot
|
|
@@ -870,60 +883,6 @@ static func _clipped_away(
|
|
|
870
883
|
return false
|
|
871
884
|
|
|
872
885
|
|
|
873
|
-
## Where [param label] draws the words [param wanted] matched in it, in the label's own space, when
|
|
874
|
-
## they are part of its text: {"point"} on their middle, {"unplaced"} saying why there is none, or
|
|
875
|
-
## {} when the words are the whole text, whose middle is the label's.
|
|
876
|
-
##
|
|
877
|
-
## A line of a dispatch names two people, each a link to their page, and a press on the label's
|
|
878
|
-
## middle landed on the second name when the first was asked for, so the game opened the wrong page.
|
|
879
|
-
## Godot answers which line a character is on, where that line is and how wide it is drawn, and
|
|
880
|
-
## nothing narrower, so the place along the line is the font's measure of the text before the
|
|
881
|
-
## words. A line in another font or holding an image measures off from what is drawn, which a wide
|
|
882
|
-
## line with nothing between the words and its start hardly does.
|
|
883
|
-
static func point_on_words(label: RichTextLabel, wanted: String) -> Dictionary:
|
|
884
|
-
var text: String = GdhSays.spaced(GdhWords.said_by(label))
|
|
885
|
-
var start: int = -1
|
|
886
|
-
var length: int = 0
|
|
887
|
-
for words: String in GdhSays.alternatives(wanted):
|
|
888
|
-
if not GdhSays.is_plain(words):
|
|
889
|
-
continue
|
|
890
|
-
var at: int = text.findn(words)
|
|
891
|
-
if at >= 0 and (start < 0 or at < start):
|
|
892
|
-
start = at
|
|
893
|
-
length = words.length()
|
|
894
|
-
if start < 0:
|
|
895
|
-
return {"unplaced": "the words were matched as a pattern, which names no one place in the text"}
|
|
896
|
-
if text.strip_edges().length() == length:
|
|
897
|
-
return {}
|
|
898
|
-
var line: int = label.get_character_line(start)
|
|
899
|
-
var shown: Vector2i = label.get_line_range(line)
|
|
900
|
-
if line < 0 or shown.x > start:
|
|
901
|
-
return {"unplaced": "the label could not say which line the words are on"}
|
|
902
|
-
var on_line: int = mini(length, shown.y - start) if shown.y > start else length
|
|
903
|
-
var font: Font = label.get_theme_font("normal_font")
|
|
904
|
-
var font_size: int = label.get_theme_font_size("normal_font_size")
|
|
905
|
-
var before: float = _width(font, font_size, text.substr(shown.x, start - shown.x))
|
|
906
|
-
var across: float = _width(font, font_size, text.substr(start, on_line))
|
|
907
|
-
var frame: StyleBox = label.get_theme_stylebox("normal")
|
|
908
|
-
var left: float = frame.get_margin(SIDE_LEFT)
|
|
909
|
-
var room: float = label.size.x - left - frame.get_margin(SIDE_RIGHT)
|
|
910
|
-
var spare: float = maxf(room - label.get_line_width(line), 0.0)
|
|
911
|
-
var aligned: float = 0.0
|
|
912
|
-
if label.horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER:
|
|
913
|
-
aligned = spare * 0.5
|
|
914
|
-
elif label.horizontal_alignment == HORIZONTAL_ALIGNMENT_RIGHT:
|
|
915
|
-
aligned = spare
|
|
916
|
-
var scrolled: float = label.get_v_scroll_bar().value if label.scroll_active else 0.0
|
|
917
|
-
var x: float = clampf(left + aligned + before + across * 0.5, left, left + room)
|
|
918
|
-
var top: float = frame.get_margin(SIDE_TOP) + label.get_line_offset(line) - scrolled
|
|
919
|
-
return {"point": Vector2(x, top + label.get_line_height(line) * 0.5)}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
## How wide [param font] at [param font_size] draws [param words] on one line.
|
|
923
|
-
static func _width(font: Font, font_size: int, words: String) -> float:
|
|
924
|
-
return font.get_string_size(words, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
925
|
-
|
|
926
|
-
|
|
927
886
|
## The canvas layer [param control] is drawn on, 0 for the viewport's own canvas.
|
|
928
887
|
static func _layer_of(control: Control) -> int:
|
|
929
888
|
var layer: CanvasLayer = control.get_canvas_layer_node()
|
|
@@ -59,6 +59,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
59
59
|
if focused != null:
|
|
60
60
|
into = str(focused.get_path())
|
|
61
61
|
|
|
62
|
+
var before: Dictionary = field_before(focused)
|
|
62
63
|
var replaced: bool = over and _select_everything(focused)
|
|
63
64
|
# Emptying a field is a real thing to ask for and the one shape of filling one in that types no
|
|
64
65
|
# characters. The selection is standing, so the key a player presses over one is what clears it.
|
|
@@ -70,7 +71,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
70
71
|
var holds: Variant = null
|
|
71
72
|
if is_instance_valid(focused):
|
|
72
73
|
holds = _what_it_holds(focused)
|
|
73
|
-
|
|
74
|
+
var answer: Dictionary = {
|
|
74
75
|
"type": "input_injected",
|
|
75
76
|
"input_type": "text",
|
|
76
77
|
"text": text,
|
|
@@ -79,6 +80,61 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
79
80
|
"replaced": replaced,
|
|
80
81
|
"holds": holds,
|
|
81
82
|
}
|
|
83
|
+
if ended_editing(focused, before):
|
|
84
|
+
answer["editing_ended"] = true
|
|
85
|
+
# A control drawing its own field takes characters in its gui_input, so typing at one is not
|
|
86
|
+
# refused; but a focused button answered that the text went into it, which read as typed.
|
|
87
|
+
if into != null and before.is_empty():
|
|
88
|
+
answer["note"] = (
|
|
89
|
+
(
|
|
90
|
+
"%s has the focus and is no LineEdit or TextEdit, so each character reached it as a key"
|
|
91
|
+
+ " press and went on to the game's input unless it kept it, and a focused button takes a"
|
|
92
|
+
+ " space or Enter as a press of it. Click a field first to type into one."
|
|
93
|
+
)
|
|
94
|
+
% into
|
|
95
|
+
)
|
|
96
|
+
return answer
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## What [param focused] says and whether it is being edited, read before keys go in, for
|
|
100
|
+
## [method field_noted] and [method ended_editing] to compare with; empty where the focus is no
|
|
101
|
+
## field. A [TextEdit] has no editing state of its own and counts as always edited.
|
|
102
|
+
static func field_before(focused: Control) -> Dictionary:
|
|
103
|
+
var field: LineEdit = focused as LineEdit
|
|
104
|
+
if field != null:
|
|
105
|
+
return {"text": field.text, "editing": field.is_editing()}
|
|
106
|
+
var box: TextEdit = focused as TextEdit
|
|
107
|
+
if box != null:
|
|
108
|
+
return {"text": box.text, "editing": true}
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Whether [param focused], read as [param before], stopped being edited, as a [LineEdit] does when
|
|
113
|
+
## it is submitted: it keeps the focus and drops every key after, so a caller reading only the focus
|
|
114
|
+
## typed next into nothing.
|
|
115
|
+
static func ended_editing(focused: Control, before: Dictionary) -> bool:
|
|
116
|
+
var was: bool = before.get("editing", false)
|
|
117
|
+
if not was or not is_instance_valid(focused):
|
|
118
|
+
return false
|
|
119
|
+
var field: LineEdit = focused as LineEdit
|
|
120
|
+
return field != null and not field.is_editing()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## [param said] with what a key did to [param focused], the field read before it as [param before]:
|
|
124
|
+
## typed_into and holds where its text changed, and editing_ended where it stopped being edited. A
|
|
125
|
+
## field being edited takes a printable key as typing and keeps it from the game's _unhandled_input,
|
|
126
|
+
## so Space into a name field pressed no jump, and an answer naming only the focus did not say why.
|
|
127
|
+
static func field_noted(said: Dictionary, focused: Control, before: Dictionary) -> Dictionary:
|
|
128
|
+
if before.is_empty() or not is_instance_valid(focused):
|
|
129
|
+
return said
|
|
130
|
+
var field: LineEdit = focused as LineEdit
|
|
131
|
+
var now: String = field.text if field != null else (focused as TextEdit).text
|
|
132
|
+
if now != before["text"]:
|
|
133
|
+
said["typed_into"] = str(focused.get_path())
|
|
134
|
+
said["holds"] = _what_it_holds(focused)
|
|
135
|
+
if ended_editing(focused, before):
|
|
136
|
+
said["editing_ended"] = true
|
|
137
|
+
return said
|
|
82
138
|
|
|
83
139
|
|
|
84
140
|
## The control keys pushed into [param viewport] go to: its focus, or the focus of the window
|
|
@@ -82,6 +82,8 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
82
82
|
var full_path: String = resource_path
|
|
83
83
|
if not full_path.begins_with("res://"):
|
|
84
84
|
full_path = "res://" + full_path
|
|
85
|
+
if DirAccess.dir_exists_absolute(full_path):
|
|
86
|
+
return _log.failure(_folder_refusal(full_path))
|
|
85
87
|
if not FileAccess.file_exists(full_path):
|
|
86
88
|
return _log.failure(
|
|
87
89
|
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
@@ -193,6 +195,11 @@ func _loaded_and_missing(script: String) -> Array[String]:
|
|
|
193
195
|
named[path] = true
|
|
194
196
|
var missing: Array[String] = []
|
|
195
197
|
for path: String in loaded:
|
|
198
|
+
# Left as written by the resolution, which is a uid the project does not know.
|
|
199
|
+
if path.begins_with("uid://"):
|
|
200
|
+
if not named.has(path) and not missing.has(path):
|
|
201
|
+
missing.append(path)
|
|
202
|
+
continue
|
|
196
203
|
if not path.begins_with("res://") or path.begins_with("res://.") or path.get_extension().is_empty():
|
|
197
204
|
continue
|
|
198
205
|
if not named.has(path) and not FileAccess.file_exists(path) and not missing.has(path):
|
|
@@ -217,6 +224,15 @@ static func _named_by_settings() -> Array[String]:
|
|
|
217
224
|
return named
|
|
218
225
|
|
|
219
226
|
|
|
227
|
+
## The refusal for [param path], a folder: a walk follows one file, and a folder was refused as a
|
|
228
|
+
## file that does not exist, about a folder the caller could see.
|
|
229
|
+
static func _folder_refusal(path: String) -> String:
|
|
230
|
+
return (
|
|
231
|
+
"%s is a folder, and project_dependencies follows one file: name a file in it."
|
|
232
|
+
% path.trim_suffix("/")
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
220
236
|
## One line for [param entry], one of [method missing_references].
|
|
221
237
|
static func missing_line(entry: Dictionary) -> String:
|
|
222
238
|
var referring: Array = entry.get("referenced_by", [])
|
|
@@ -235,6 +251,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
235
251
|
|
|
236
252
|
if not resource_path.begins_with("res://"):
|
|
237
253
|
resource_path = "res://" + resource_path
|
|
254
|
+
if DirAccess.dir_exists_absolute(resource_path):
|
|
255
|
+
return _log.failure(_folder_refusal(resource_path))
|
|
238
256
|
# A file that is gone is searched for all the same, since what still refers to it is what a
|
|
239
257
|
# caller asks about one, and the forward walk of a scene had just named it missing. Refused only
|
|
240
258
|
# when nothing refers to it either, which is a path written wrong.
|
|
@@ -331,6 +349,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
331
349
|
kind = _path_reference_kind(line, file_path)
|
|
332
350
|
if not loaded.is_empty() and written_path != loaded:
|
|
333
351
|
extra["stale_path"] = written_path
|
|
352
|
+
var unknown_uid: String = (
|
|
353
|
+
_unknown_uid_of(line) if trimmed.begins_with("[ext_resource") else ""
|
|
354
|
+
)
|
|
355
|
+
if not unknown_uid.is_empty():
|
|
356
|
+
extra["unknown_uid"] = unknown_uid
|
|
334
357
|
elif by_its_autoload:
|
|
335
358
|
kind = "autoload"
|
|
336
359
|
else:
|
|
@@ -570,6 +593,7 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
570
593
|
elif path.get_extension() in SHADER_KINDS:
|
|
571
594
|
content = _shader_code(content)
|
|
572
595
|
var moved: Dictionary = _moved_by_uid(content)
|
|
596
|
+
var unknown: Dictionary = _unknown_uids(content, path)
|
|
573
597
|
|
|
574
598
|
for pattern: String in REFERENCE_PATTERNS:
|
|
575
599
|
var regex: RegEx = GdhPatterns.compiled(pattern)
|
|
@@ -577,6 +601,12 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
577
601
|
var written: String = _resolved(_referenced_path(m.get_string()), path)
|
|
578
602
|
var dep_path: String = str(moved.get(written, written))
|
|
579
603
|
|
|
604
|
+
# A uid the project does not know names no file, and loading it fails: passed over as
|
|
605
|
+
# no res:// path, a script's load of one went unsaid while the game printed
|
|
606
|
+
# "Unrecognized UID" at that line.
|
|
607
|
+
if dep_path.begins_with("uid://"):
|
|
608
|
+
_depend(deps, path, dep_path, {}, current_depth, walk)
|
|
609
|
+
continue
|
|
580
610
|
if not dep_path.begins_with("res://"):
|
|
581
611
|
continue
|
|
582
612
|
# A literal naming a directory, or the start of a path a script builds as it runs
|
|
@@ -590,14 +620,10 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
590
620
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
591
621
|
continue
|
|
592
622
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
{"stale_path": written} if dep_path != written else {},
|
|
598
|
-
current_depth,
|
|
599
|
-
walk
|
|
600
|
-
)
|
|
623
|
+
var named_by: Dictionary = {"stale_path": written} if dep_path != written else {}
|
|
624
|
+
if unknown.has(dep_path):
|
|
625
|
+
named_by["unknown_uid"] = unknown[dep_path]
|
|
626
|
+
_depend(deps, path, dep_path, named_by, current_depth, walk)
|
|
601
627
|
|
|
602
628
|
# A script uses another by its class_name as surely as by a preload, and only the reverse walk
|
|
603
629
|
# looked for that: asked what a script depends on, the answer left out the script it typed a
|
|
@@ -633,7 +659,11 @@ func _depend(
|
|
|
633
659
|
for existing: Dictionary in deps:
|
|
634
660
|
if existing.get("path", "") == dep_path:
|
|
635
661
|
return
|
|
636
|
-
|
|
662
|
+
# A uid still written as one is one the project does not know, and asked about, the engine prints
|
|
663
|
+
# an error for it.
|
|
664
|
+
var dep_info: Dictionary = {
|
|
665
|
+
"path": dep_path, "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path)
|
|
666
|
+
}
|
|
637
667
|
dep_info.merge(named_by)
|
|
638
668
|
# Marked where the reference closing the cycle is. Marked inside the file reached again, the tree
|
|
639
669
|
# answered that file as a dependency of itself.
|
|
@@ -743,6 +773,25 @@ static func _moved_by_uid(content: String) -> Dictionary:
|
|
|
743
773
|
return moved
|
|
744
774
|
|
|
745
775
|
|
|
776
|
+
# The ext_resource paths in [param content], read from [param from], whose uid names no file the
|
|
777
|
+
# project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
|
|
778
|
+
# the walk followed the path saying nothing, so an export was the first to mention it.
|
|
779
|
+
static func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
780
|
+
var unknown: Dictionary = {}
|
|
781
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
782
|
+
var uid: String = _unknown_uid_of(line.get_string())
|
|
783
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
|
|
784
|
+
if at != null and not uid.is_empty():
|
|
785
|
+
unknown[_resolved(at.get_string(1), from)] = uid
|
|
786
|
+
return unknown
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
# The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
|
|
790
|
+
static func _unknown_uid_of(entry: String) -> String:
|
|
791
|
+
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
792
|
+
return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
|
|
793
|
+
|
|
794
|
+
|
|
746
795
|
# The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
|
|
747
796
|
# the project knows, and the engine falls back to the path.
|
|
748
797
|
static func _loaded_by_uid(entry: String) -> String:
|
package/build/keeper.js
CHANGED
|
@@ -9190,7 +9190,7 @@ class GameDebugger {
|
|
|
9190
9190
|
case "stack": {
|
|
9191
9191
|
const frames = await this.stack();
|
|
9192
9192
|
if (frames === null) {
|
|
9193
|
-
return this.notHeld();
|
|
9193
|
+
return this.notHeld("stack to read");
|
|
9194
9194
|
}
|
|
9195
9195
|
return {
|
|
9196
9196
|
held: this.heldState(),
|
|
@@ -9218,9 +9218,9 @@ class GameDebugger {
|
|
|
9218
9218
|
...this.held.canContinue ? {} : { canContinue: false }
|
|
9219
9219
|
};
|
|
9220
9220
|
}
|
|
9221
|
-
notHeld() {
|
|
9221
|
+
notHeld(what) {
|
|
9222
9222
|
return {
|
|
9223
|
-
refused:
|
|
9223
|
+
refused: `The game is running, not held, so it has no ${what}: debug_control pause stops it where it is, and a breakpoint stops it on its line.`
|
|
9224
9224
|
};
|
|
9225
9225
|
}
|
|
9226
9226
|
async stack() {
|
|
@@ -9384,7 +9384,7 @@ class GameDebugger {
|
|
|
9384
9384
|
async variables(frame) {
|
|
9385
9385
|
const held = this.held;
|
|
9386
9386
|
if (held === null) {
|
|
9387
|
-
return this.notHeld();
|
|
9387
|
+
return this.notHeld("frame whose variables could be read");
|
|
9388
9388
|
}
|
|
9389
9389
|
const frames = await this.stack();
|
|
9390
9390
|
if (frames === null) {
|
|
@@ -9431,7 +9431,7 @@ class GameDebugger {
|
|
|
9431
9431
|
async evaluate(frame, expression) {
|
|
9432
9432
|
const held = this.held;
|
|
9433
9433
|
if (held === null) {
|
|
9434
|
-
return this.notHeld();
|
|
9434
|
+
return this.notHeld("frame to work an expression out in");
|
|
9435
9435
|
}
|
|
9436
9436
|
const read = await this.variables(frame);
|
|
9437
9437
|
if (!Array.isArray(read["scopes"])) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.38",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|