gdharness 1.3.30 → 1.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -10204,7 +10204,7 @@ function heldAtExit(lines) {
|
|
|
10204
10204
|
objectsSaid.length === 0 ? undefined : `objects left alive: ${objectsSaid.join(", ")}`,
|
|
10205
10205
|
named.length === 0 ? undefined : `resources still in use: ${named.join(", ")}${more > 0 ? `, and ${String(more)} more` : ""}`
|
|
10206
10206
|
].filter((part) => part !== undefined);
|
|
10207
|
-
return `The engine named them: ${parts.join("; ")}
|
|
10207
|
+
return `The engine named them: ${parts.join("; ")}.${objects.has("RefCounted") ? ` ${REFCOUNTED_HELD}` : ""}`;
|
|
10208
10208
|
}
|
|
10209
10209
|
function selfTypedExports(sources) {
|
|
10210
10210
|
const found = [];
|
|
@@ -10238,13 +10238,14 @@ function selfTypedSentence(found) {
|
|
|
10238
10238
|
const one = found.length === 1;
|
|
10239
10239
|
return `Godot never frees a script exporting an array or dictionary typed with its own class once a run ` + `has loaded it, and reports it as leaked at exit: ${named} ${one ? "does" : "do"}, so a run loading ` + `${one ? "it" : "one"} ends with this report whatever else it frees. Typed with a class it extends ` + `instead, as Array[Resource], ${one ? "it is" : "each is"} freed.`;
|
|
10240
10240
|
}
|
|
10241
|
-
var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scripts", WALK_ENDED = "gdharness: the engine's shutdown cleared every script", LEAKED_INSTANCE, STILL_IN_USE, IN_USE_NAMED = 8, WALK_STOPPED_SENTENCE, NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*", EXPORTED;
|
|
10241
|
+
var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scripts", WALK_ENDED = "gdharness: the engine's shutdown cleared every script", LEAKED_INSTANCE, STILL_IN_USE, IN_USE_NAMED = 8, REFCOUNTED_HELD, WALK_STOPPED_SENTENCE, NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*", EXPORTED;
|
|
10242
10242
|
var init_engine_leaks = __esm(() => {
|
|
10243
10243
|
init_class_cache();
|
|
10244
10244
|
init_gdscript_source();
|
|
10245
10245
|
EXIT_LEAK = /ObjectDB instances? (?:was |were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit|RIDs? of type ["'][^"']+["'] (?:was|were) leaked/;
|
|
10246
10246
|
LEAKED_INSTANCE = /^Leaked instance: (\w+):/;
|
|
10247
10247
|
STILL_IN_USE = /^Resource still in use: (.+?) \(\w+\)$/;
|
|
10248
|
+
REFCOUNTED_HELD = "A RefCounted alive at exit is held by a cycle of references (two holding each other, or one " + "holding a lambda that captures it) or by an Object never freed, rather than by a node left out " + "of the tree; a WeakRef on one side of the cycle, or clearing the reference once done, frees it.";
|
|
10248
10249
|
WALK_STOPPED_SENTENCE = "The engine's shutdown stopped part way through its scripts (godot#117991, fixed in 4.8 by " + "godot#120976), so it reported as leaked at exit what it never got to free, and the report is the " + "engine's rather than the run's. gdharness holds every script loaded from a file past that walk, " + "which keeps it whole, so this one stopped on a script it could not hold, such as one made in code. " + "A leak of the run's own is not told apart in such a run.";
|
|
10249
10250
|
EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
|
|
10250
10251
|
});
|
|
@@ -10391,24 +10392,50 @@ function ignoredYetRan(report, ignored) {
|
|
|
10391
10392
|
}
|
|
10392
10393
|
return found;
|
|
10393
10394
|
}
|
|
10394
|
-
function
|
|
10395
|
-
const
|
|
10395
|
+
function rowNamed(entry) {
|
|
10396
|
+
const named = /^(.+):([A-Za-z_]\w*):(\d+)(?:\s.*)?$/.exec(entry.replace("res://", ""));
|
|
10397
|
+
return named === null ? null : { suite: named[1] ?? "", test: named[2] ?? "", row: Number(named[3]) };
|
|
10398
|
+
}
|
|
10399
|
+
function declaring(declared, suite, test) {
|
|
10400
|
+
return declared.filter((one) => suiteNameOf(one.path) === suite || one.path.includes(suite)).flatMap((one) => one.cases.filter((name) => test === null || name === test).map((name) => ({ path: one.path, test: name })));
|
|
10401
|
+
}
|
|
10402
|
+
function ignoredForTheRunner(ignored, declared) {
|
|
10403
|
+
return ignored.flatMap((entry) => {
|
|
10404
|
+
const row = rowNamed(entry);
|
|
10405
|
+
return row === null ? [entry] : declaring(declared, row.suite, row.test).map((one) => `${one.path}:${one.test}:${String(row.row)}`);
|
|
10406
|
+
});
|
|
10407
|
+
}
|
|
10408
|
+
function rowsIgnoredIn(printed) {
|
|
10409
|
+
return new Set(printed.flatMap((line) => IGNORED_ROW_LINE.exec(line)?.[1] ?? []));
|
|
10410
|
+
}
|
|
10411
|
+
function ignoredLeftOut(report, ignored, declared, rowsIgnored = new Set) {
|
|
10396
10412
|
const leftOut = new Set;
|
|
10397
10413
|
const unmatched = [];
|
|
10398
10414
|
for (const entry of ignored) {
|
|
10415
|
+
const row = rowNamed(entry);
|
|
10416
|
+
if (row !== null) {
|
|
10417
|
+
const rows = declaring(declared, row.suite, row.test).filter((one) => rowsIgnored.has(`${one.path} > ${one.test}:${String(row.row)}`));
|
|
10418
|
+
if (rows.length === 0) {
|
|
10419
|
+
unmatched.push(entry);
|
|
10420
|
+
}
|
|
10421
|
+
for (const one of rows) {
|
|
10422
|
+
leftOut.add(`${suiteNameOf(one.path)}:${one.test}:${String(row.row)}`);
|
|
10423
|
+
}
|
|
10424
|
+
continue;
|
|
10425
|
+
}
|
|
10399
10426
|
const bare = entry.replace("res://", "");
|
|
10400
10427
|
const at = bare.lastIndexOf(":");
|
|
10401
10428
|
const suiteNamed = at === -1 ? bare : bare.slice(0, at);
|
|
10402
10429
|
const testNamed = at === -1 ? null : bare.slice(at + 1);
|
|
10403
|
-
const named = declared
|
|
10430
|
+
const named = declaring(declared, suiteNamed, testNamed);
|
|
10404
10431
|
if (named.length === 0) {
|
|
10405
10432
|
unmatched.push(entry);
|
|
10406
10433
|
continue;
|
|
10407
10434
|
}
|
|
10408
|
-
for (const {
|
|
10409
|
-
const reported = report.suites.filter((ran) => ran.path ===
|
|
10435
|
+
for (const { path, test } of named) {
|
|
10436
|
+
const reported = report.suites.filter((ran) => ran.path === path || ran.path === null && ran.name === suiteNameOf(path)).some((ran) => ran.cases.some((one) => one.name === test || new RegExp(`^${test}\\W`).test(one.name)));
|
|
10410
10437
|
if (!reported) {
|
|
10411
|
-
leftOut.add(`${
|
|
10438
|
+
leftOut.add(`${suiteNameOf(path)}:${test}`);
|
|
10412
10439
|
}
|
|
10413
10440
|
}
|
|
10414
10441
|
}
|
|
@@ -10756,7 +10783,7 @@ function testProgressMessage(printed, elapsedMs) {
|
|
|
10756
10783
|
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`}`;
|
|
10757
10784
|
return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
|
|
10758
10785
|
}
|
|
10759
|
-
var MalformedReportError, ENTITIES, ATTRIBUTE, 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;
|
|
10786
|
+
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;
|
|
10760
10787
|
var init_junit = __esm(() => {
|
|
10761
10788
|
MalformedReportError = class MalformedReportError extends Error {
|
|
10762
10789
|
constructor(message) {
|
|
@@ -10766,6 +10793,7 @@ var init_junit = __esm(() => {
|
|
|
10766
10793
|
};
|
|
10767
10794
|
ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
10768
10795
|
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
10796
|
+
IGNORED_ROW_LINE = /^gdharness ignored (res:\/\/.+?\.gd > [^:\s]+:\d+)\s*$/;
|
|
10769
10797
|
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
10770
10798
|
NOTHING_RAN = /no test cases found/i;
|
|
10771
10799
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
@@ -16648,7 +16676,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16648
16676
|
title: "Run tests",
|
|
16649
16677
|
effect: "additive",
|
|
16650
16678
|
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.",
|
|
16651
|
-
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, 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. 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. 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,
|
|
16679
|
+
answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, 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. 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,
|
|
16652
16680
|
parameters: {
|
|
16653
16681
|
projectPath: PROJECT_PATH,
|
|
16654
16682
|
path: {
|
|
@@ -16659,7 +16687,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16659
16687
|
ignore: {
|
|
16660
16688
|
type: "array",
|
|
16661
16689
|
items: { type: "string" },
|
|
16662
|
-
description: 'Suites or
|
|
16690
|
+
description: 'Suites, cases or rows to leave out, as "suite_name", "suite_name:test_name", or "suite_name:test_name:N" for one row of a parameterised case, N being the zero-based index gdUnit4 reports the row under: a failure named "test_name:2 (3, 4)" is the third row, and the name can be passed back as reported. A bare test name is read as a suite name and leaves out nothing; an entry that named a case which ran all the same comes back under ignoreNotApplied with the form that would have left it out. Every case the list left out comes back under ignored as suite_name:test_name, and every row as suite_name:test_name:N, so a run called passed says what it did not run, and an entry naming no suite, case or row under path comes back under ignoreUnmatched.'
|
|
16663
16691
|
},
|
|
16664
16692
|
tests: {
|
|
16665
16693
|
type: "array",
|
|
@@ -16669,7 +16697,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16669
16697
|
engineArgs: {
|
|
16670
16698
|
type: "array",
|
|
16671
16699
|
items: { type: "string" },
|
|
16672
|
-
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well,
|
|
16700
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, the newest of them, and a leak at exit is named in the note: the objects left alive counted by class, and the resources still in use, the project's own before its addons'.`
|
|
16673
16701
|
},
|
|
16674
16702
|
failFast: {
|
|
16675
16703
|
type: "boolean",
|
|
@@ -17653,7 +17681,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17653
17681
|
name: "runtime_inspect",
|
|
17654
17682
|
title: "Inspect the running game",
|
|
17655
17683
|
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, the performance metrics, or what it tells a screen reader. Needs the game running with the runtime addon.",
|
|
17656
|
-
answer: "text: lines, count, and under omitted how many lines the limit left behind; out_of_view counts the lines left out for being drawn outside the viewport or a ScrollContainer, with a note saying so. find: nodes and count, with hidden counting the matches left out as hidden and out_of_view those left out as drawn out of view, each with a note. rect: canvas and window, and for a Control visible and in_view, false when it is drawn wholly outside its viewport or a ScrollContainer holding it. metrics: frame_time is the mean wall time of the last frames_timed frames (up to 120; the game, physics and the wait for the display included), frame_time_max the slowest, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. process_frames and physics_frames count frames and physics steps since the start; get_frames_drawn() stays 0 headless. physics_steps_per_second is the physics steps a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, set once a second: the last second's frames and its slowest process and physics step, so they trail a change by up to a second, and the last two are worst cases, not the frame. memory_static is the engine's CPU-side allocation in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are the renderer's GPU memory in bytes, which memory_static leaves out. accessibility: under windows, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, value_text (a range's spoken words, such as \"75%\"), description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the focused one. node is the node that made the element; a part a node made of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name reads as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused.",
|
|
17684
|
+
answer: "text: lines, count, and under omitted how many lines the limit left behind; out_of_view counts the lines left out for being drawn outside the viewport or a ScrollContainer, with a note saying so. find: nodes and count, with hidden counting the matches left out as hidden and out_of_view those left out as drawn out of view, each with a note. rect: canvas and window, and for a Control visible and in_view, false when it is drawn wholly outside its viewport or a ScrollContainer holding it. metrics: frame_time is the mean wall time of the last frames_timed frames (up to 120; the game, physics and the wait for the display included), frame_time_max the slowest, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. process_frames and physics_frames count frames and physics steps since the start; get_frames_drawn() stays 0 headless. physics_steps_per_second is the physics steps a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, set once a second: the last second's frames and its slowest process and physics step, so they trail a change by up to a second, and the last two are worst cases, not the frame. memory_static is the engine's CPU-side allocation in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are the renderer's GPU memory in bytes, which memory_static leaves out. accessibility: under windows, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, value_text (a range's spoken words, such as \"75%\"), description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the focused one. node is the node that made the element; a part a node made of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name reads as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. tookMsBy splits the read's milliseconds into starting PowerShell (start), loading UI Automation (load) and walking the windows (walk); a read stopped at its deadline says instead which of those it had reached.",
|
|
17657
17685
|
parameters: {
|
|
17658
17686
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17659
17687
|
pid: RUNNING_PID,
|
|
@@ -17722,7 +17750,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17722
17750
|
timeoutMs: {
|
|
17723
17751
|
type: "integer",
|
|
17724
17752
|
minimum: 1,
|
|
17725
|
-
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS; accessibility defaults to
|
|
17753
|
+
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS; accessibility defaults to 75000 or that, whichever is longer, since a cold PowerShell on a loaded machine can take most of a minute to start."
|
|
17726
17754
|
}
|
|
17727
17755
|
},
|
|
17728
17756
|
requires: [],
|
|
@@ -51186,7 +51214,7 @@ class GodotServer {
|
|
|
51186
51214
|
}, {
|
|
51187
51215
|
tool: "runtime_inspect",
|
|
51188
51216
|
op,
|
|
51189
|
-
patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(
|
|
51217
|
+
patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(75000, this.runtimeTimeoutMs())
|
|
51190
51218
|
});
|
|
51191
51219
|
default:
|
|
51192
51220
|
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
|
|
@@ -52512,6 +52540,7 @@ ${platform}`;
|
|
|
52512
52540
|
const ours = `gdharness-reports/${randomUUID2()}`;
|
|
52513
52541
|
const reports = `res://.godot/${ours}`;
|
|
52514
52542
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
52543
|
+
const declaredUnder = ignored.length === 0 ? [] : asked.flatMap((path) => path.endsWith(".gd") ? projectScripts.suite(path) ?? [] : projectScripts.suitesUnder([path]));
|
|
52515
52544
|
const userData = scratchDirectory("tests");
|
|
52516
52545
|
const suitesFile = join43(userData, "suites.txt");
|
|
52517
52546
|
const skippingFile = join43(userData, "skipping.txt");
|
|
@@ -52520,7 +52549,7 @@ ${platform}`;
|
|
|
52520
52549
|
writeFileSync22(suitesFile, `${(selection.value?.suites ?? asked).join(`
|
|
52521
52550
|
`)}
|
|
52522
52551
|
`);
|
|
52523
|
-
writeFileSync22(skippingFile, `${[...ignored, ...selection.value?.ignore ?? []].join(`
|
|
52552
|
+
writeFileSync22(skippingFile, `${[...ignoredForTheRunner(ignored, declaredUnder), ...selection.value?.ignore ?? []].join(`
|
|
52524
52553
|
`)}
|
|
52525
52554
|
`);
|
|
52526
52555
|
const viewport = windowed ? viewportOf(project.value.path) : null;
|
|
@@ -52673,7 +52702,7 @@ ${platform}`;
|
|
|
52673
52702
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
52674
52703
|
const ranAs = notCompiled !== null ? `the autoload ${notCompiled.name} (${notCompiled.path}) did not compile, so no result counts` : cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? howItExited(run);
|
|
52675
52704
|
const nothingAt = missing.join(" and ");
|
|
52676
|
-
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
52705
|
+
const verdict = missing.length === 0 ? ranAs : ranAs === "passed" ? `nothing at ${nothingAt}` : `${ranAs}; nothing at ${nothingAt}`;
|
|
52677
52706
|
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
52678
52707
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
52679
52708
|
const marker = readNonEmptyString(args, "printed");
|
|
@@ -52782,6 +52811,7 @@ ${error.text}`;
|
|
|
52782
52811
|
];
|
|
52783
52812
|
const walkStopped = leakedAtExit.length > 0 && walkStoppedPartWay(run.log.all.map((entry) => entry.text));
|
|
52784
52813
|
const countedLeaks = walkStopped ? [] : leakedAtExit;
|
|
52814
|
+
const heldNamed = countedLeaks.length > 0 ? heldAtExit(run.log.all.map((entry) => entry.text)) : undefined;
|
|
52785
52815
|
const declaredNotPrinted = judgedErrors.neverPrinted.map((missing) => ({
|
|
52786
52816
|
suite: missing.suite,
|
|
52787
52817
|
case: missing.during,
|
|
@@ -52812,8 +52842,7 @@ ${error.text}`;
|
|
|
52812
52842
|
const uncaughtFolded = foldedRepeats(uncaught.map(aboveTheRunner));
|
|
52813
52843
|
const listedAsUncaught = new Set(uncaughtFolded.map((entry) => entry.index));
|
|
52814
52844
|
const notApplied = ignoredYetRan(report, ignored);
|
|
52815
|
-
const
|
|
52816
|
-
const ignoreRead = ignoredLeftOut(report, ignored, asked.flatMap((path) => scripts === null ? [] : path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path])));
|
|
52845
|
+
const ignoreRead = ignoredLeftOut(report, ignored, declaredUnder, rowsIgnoredIn(said));
|
|
52817
52846
|
const unmatched = ignoreRead.unmatched.filter((entry) => !notApplied.some((one) => one.entry === entry));
|
|
52818
52847
|
const testsNotRun = [
|
|
52819
52848
|
...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}`))
|
|
@@ -52821,7 +52850,7 @@ ${error.text}`;
|
|
|
52821
52850
|
return this.jsonTextResponse({
|
|
52822
52851
|
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,
|
|
52823
52852
|
verdict: [
|
|
52824
|
-
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length > 0 ? undefined : verdict,
|
|
52853
|
+
verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length > 0 ? undefined : verdict,
|
|
52825
52854
|
...[false, true].map((warning) => {
|
|
52826
52855
|
const count = undeclaredPrinted.filter((error) => error.warning === warning).length;
|
|
52827
52856
|
const word = warning ? "warning" : "error";
|
|
@@ -52870,13 +52899,13 @@ ${error.text}`;
|
|
|
52870
52899
|
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,
|
|
52871
52900
|
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,
|
|
52872
52901
|
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,
|
|
52873
|
-
unmatched.length > 0 ? `${unmatched.join(", ")} ${unmatched.length === 1 ? "names" : "name"} no suite or
|
|
52902
|
+
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,
|
|
52874
52903
|
uncaughtNote(uncaught),
|
|
52875
52904
|
undeclaredErrors.length > 0 ? `An engine error or warning a suite does not declare fails the run, as gdharness test judges it; ${declaringAdvice(undeclaredErrors[0]?.case ?? "test_name", undeclaredErrors[0]?.warning === true)}, once for each time it prints it${undeclaredErrors.some((error) => error.warning !== undeclaredErrors[0]?.warning) ? undeclaredErrors[0]?.warning === true ? ", and an error in its GDHARNESS_EXPECTED_ERRORS" : ", and a warning in its GDHARNESS_EXPECTED_WARNINGS" : ""}.` : undefined,
|
|
52876
52905
|
declaredNotPrinted.length > 0 ? "A declared error or warning the case did not print fails the run too, since the case no longer reaches what it was written to test." : undefined,
|
|
52877
52906
|
walkStopped ? WALK_STOPPED_SENTENCE : undefined,
|
|
52878
52907
|
countedLeaks.length > 0 ? [
|
|
52879
|
-
'What the engine still held as it exited fails the run, as gdharness test judges it; engineArgs ["--verbose"]
|
|
52908
|
+
heldNamed === undefined ? 'What the engine still held as it exited fails the run, as gdharness test judges it; engineArgs ["--verbose"] has the engine name what it held.' : `What the engine still held as it exited fails the run, as gdharness test judges it. ${heldNamed}`,
|
|
52880
52909
|
selfTypedSentence(projectSelfTypedExports(project.value.path))
|
|
52881
52910
|
].filter((part) => part !== undefined).join(" ") : undefined,
|
|
52882
52911
|
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
@@ -18,9 +18,15 @@ const SETTLE_MS: int = 8000
|
|
|
18
18
|
## How long a window with nothing in it is walked before the client stops: a game building its tree
|
|
19
19
|
## fills the window within a few frames of being asked, and one building none never does.
|
|
20
20
|
const EMPTY_MS: int = 4000
|
|
21
|
-
## How long
|
|
22
|
-
##
|
|
23
|
-
|
|
21
|
+
## How long starting PowerShell and loading UI Automation may take before the client is stopped. It
|
|
22
|
+
## waits on the machine alone: a warm start takes under a second, and a cold one on a loaded Windows
|
|
23
|
+
## runner took the whole of a single 23 s budget meant for the walk as well.
|
|
24
|
+
const START_MS: int = 45000
|
|
25
|
+
## How long the walks may take beyond [constant SETTLE_MS] a window once UI Automation is loaded,
|
|
26
|
+
## kept short since a walk that overruns is a window that does not answer.
|
|
27
|
+
const WALK_SLACK_MS: int = 7000
|
|
28
|
+
## How often the progress file is read while the client runs.
|
|
29
|
+
const LOOK_MS: int = 250
|
|
24
30
|
## Deep enough for any interface a game builds, and inside the hundred levels ConvertTo-Json stops
|
|
25
31
|
## at, which are two to a level of the tree.
|
|
26
32
|
const MOST_LEVELS: int = 40
|
|
@@ -28,16 +34,23 @@ const MOST_LEVELS: int = 40
|
|
|
28
34
|
## answer goes.
|
|
29
35
|
const NODE_ID: String = "_node_instance"
|
|
30
36
|
|
|
31
|
-
## The client, as PowerShell, with $Handles, $Out, $SettleMs, $EmptyMs and $MostLevels
|
|
32
|
-
## element answers what UI Automation names its role, its name, its value where it
|
|
33
|
-
## bounds in the window's pixels, and the actions a screen reader is offered on it.
|
|
37
|
+
## The client, as PowerShell, with $Handles, $Out, $Progress, $SettleMs, $EmptyMs and $MostLevels
|
|
38
|
+
## set before it. Each element answers what UI Automation names its role, its name, its value where it
|
|
39
|
+
## has one, its bounds in the window's pixels, and the actions a screen reader is offered on it. Each
|
|
40
|
+
## stage it reaches is marked in $Progress as it is reached, with the time, so a client stopped at the
|
|
41
|
+
## deadline still says how far it got.
|
|
34
42
|
const WALK: String = r"""
|
|
35
43
|
$ErrorActionPreference = 'Stop'
|
|
36
44
|
function Answer($value) {
|
|
37
45
|
[System.IO.File]::WriteAllText($Out, (ConvertTo-Json -InputObject $value -Depth 100 -Compress))
|
|
38
46
|
}
|
|
47
|
+
function Mark($stage) {
|
|
48
|
+
[System.IO.File]::AppendAllText($Progress, "$stage $([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())`n")
|
|
49
|
+
}
|
|
39
50
|
try {
|
|
51
|
+
Mark 'began'
|
|
40
52
|
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
|
|
53
|
+
Mark 'loaded'
|
|
41
54
|
$walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
|
|
42
55
|
function Patterned($element, $name) {
|
|
43
56
|
$pattern = ("System.Windows.Automation.$($name)Pattern" -as [type])::Pattern
|
|
@@ -148,6 +161,7 @@ try {
|
|
|
148
161
|
$r = $root.Current.BoundingRectangle
|
|
149
162
|
$tree = Describe $root $r.X $r.Y 0
|
|
150
163
|
$last = Count $tree
|
|
164
|
+
Mark "walked $last"
|
|
151
165
|
# Walked again until two walks in a row agree, since the first question is what makes the
|
|
152
166
|
# engine build the tree, which arrives over the frames after it.
|
|
153
167
|
while ([DateTime]::UtcNow -lt $deadline) {
|
|
@@ -155,6 +169,7 @@ try {
|
|
|
155
169
|
$r = $root.Current.BoundingRectangle
|
|
156
170
|
$tree = Describe $root $r.X $r.Y 0
|
|
157
171
|
$count = Count $tree
|
|
172
|
+
Mark "walked $count"
|
|
158
173
|
if ($count -gt 1 -and $count -eq $last) { break }
|
|
159
174
|
if ($count -eq 1 -and [DateTime]::UtcNow -gt $empty) { break }
|
|
160
175
|
$last = $count
|
|
@@ -221,13 +236,22 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
221
236
|
var out: String = OS.get_temp_dir().path_join(
|
|
222
237
|
"gdharness-accessibility-%d-%d.json" % [OS.get_process_id(), Time.get_ticks_usec()]
|
|
223
238
|
)
|
|
239
|
+
var progress: String = out + ".progress"
|
|
224
240
|
var script: String = (
|
|
225
241
|
(
|
|
226
|
-
"$Handles = @(%s)\n$Out = '%s'\n$SettleMs = %d\n$EmptyMs = %d\n$MostLevels = %d\n"
|
|
227
|
-
% [
|
|
242
|
+
"$Handles = @(%s)\n$Out = '%s'\n$Progress = '%s'\n$SettleMs = %d\n$EmptyMs = %d\n$MostLevels = %d\n"
|
|
243
|
+
% [
|
|
244
|
+
", ".join(handles),
|
|
245
|
+
out.replace("'", "''"),
|
|
246
|
+
progress.replace("'", "''"),
|
|
247
|
+
SETTLE_MS,
|
|
248
|
+
EMPTY_MS,
|
|
249
|
+
MOST_LEVELS,
|
|
250
|
+
]
|
|
228
251
|
)
|
|
229
252
|
+ WALK
|
|
230
253
|
)
|
|
254
|
+
var started_ms: int = int(Time.get_unix_time_from_system() * 1000.0)
|
|
231
255
|
# The system's own PowerShell by its path, so a powershell.exe earlier on PATH is not what runs.
|
|
232
256
|
var shell: String = OS.get_environment("SystemRoot").path_join(
|
|
233
257
|
"System32/WindowsPowerShell/v1.0/powershell.exe"
|
|
@@ -248,18 +272,38 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
248
272
|
return _refused("PowerShell could not be started from %s to read the tree" % shell)
|
|
249
273
|
# Waited out a frame at a time rather than blocking: the client's questions are answered on the
|
|
250
274
|
# game's main thread, which a blocking wait would hold, and neither side would move.
|
|
251
|
-
|
|
275
|
+
# Two budgets, so a slow start is never charged to the walk: one for PowerShell to begin and load
|
|
276
|
+
# UI Automation, and once it has, one for the walks.
|
|
277
|
+
var began_at: int = Time.get_ticks_msec()
|
|
278
|
+
var deadline: int = _deadline(began_at, -1, handles.size())
|
|
279
|
+
var walking: bool = false
|
|
280
|
+
var looked_at: int = began_at
|
|
252
281
|
while OS.is_process_running(pid):
|
|
253
|
-
|
|
282
|
+
var now: int = Time.get_ticks_msec()
|
|
283
|
+
if not walking and now - looked_at >= LOOK_MS:
|
|
284
|
+
looked_at = now
|
|
285
|
+
if FileAccess.get_file_as_string(progress).contains("loaded "):
|
|
286
|
+
walking = true
|
|
287
|
+
deadline = _deadline(began_at, now, handles.size())
|
|
288
|
+
if now > deadline:
|
|
254
289
|
var _killed: Error = OS.kill(pid)
|
|
290
|
+
var reached: String = _reached(FileAccess.get_file_as_string(progress), started_ms)
|
|
291
|
+
var _gone: Error = DirAccess.remove_absolute(progress)
|
|
255
292
|
return _refused(
|
|
256
|
-
|
|
293
|
+
(
|
|
294
|
+
"UI Automation had not read the tree after %d ms, so it was stopped: %s"
|
|
295
|
+
% [now - began_at, reached]
|
|
296
|
+
)
|
|
257
297
|
)
|
|
258
298
|
await _host.get_tree().process_frame
|
|
259
299
|
|
|
260
300
|
var text: String = FileAccess.get_file_as_string(out)
|
|
301
|
+
var took: Dictionary = _took(
|
|
302
|
+
FileAccess.get_file_as_string(progress), started_ms, int(Time.get_unix_time_from_system() * 1000.0)
|
|
303
|
+
)
|
|
261
304
|
# Not checked: a file that is not there was never written, which the answer below says.
|
|
262
305
|
var _removed: Error = DirAccess.remove_absolute(out)
|
|
306
|
+
var _progress_removed: Error = DirAccess.remove_absolute(progress)
|
|
263
307
|
var read: Variant = GdhRead.json_or_null(text)
|
|
264
308
|
if not read is Dictionary:
|
|
265
309
|
return _refused("UI Automation answered nothing readable: %s" % text.left(500))
|
|
@@ -290,6 +334,8 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
290
334
|
elements += _count(window)
|
|
291
335
|
unnamed += _unnamed_text(window)
|
|
292
336
|
var result: Dictionary = {"windows": windows, "elements": elements}
|
|
337
|
+
if not took.is_empty():
|
|
338
|
+
result["tookMsBy"] = took
|
|
293
339
|
if placing.tooltips > 0:
|
|
294
340
|
result["tooltip_note"] = (
|
|
295
341
|
"tooltip is the node's tooltip_text, which the engine puts in the tree, and UI Automation, as"
|
|
@@ -664,5 +710,83 @@ static func _unnamed_text(element: Dictionary) -> int:
|
|
|
664
710
|
return total
|
|
665
711
|
|
|
666
712
|
|
|
713
|
+
## When a client started at [param began_at] is stopped, in ticks: [constant START_MS] after it began
|
|
714
|
+
## while UI Automation is loading ([param loaded_at] -1), and once it was seen loaded at
|
|
715
|
+
## [param loaded_at], [constant SETTLE_MS] for each of [param windows] and [constant WALK_SLACK_MS]
|
|
716
|
+
## after that.
|
|
717
|
+
static func _deadline(began_at: int, loaded_at: int, windows: int) -> int:
|
|
718
|
+
if loaded_at < 0:
|
|
719
|
+
return began_at + START_MS
|
|
720
|
+
return loaded_at + SETTLE_MS * maxi(1, windows) + WALK_SLACK_MS
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
## The stages [param progress] marks, each with the Unix time in ms it was reached, as an array of
|
|
724
|
+
## [stage, ms] in order.
|
|
725
|
+
static func _marks(progress: String) -> Array:
|
|
726
|
+
var marks: Array = []
|
|
727
|
+
for line: String in progress.split("\n", false):
|
|
728
|
+
var words: PackedStringArray = line.strip_edges().split(" ", false)
|
|
729
|
+
if words.size() >= 2 and words[words.size() - 1].is_valid_int():
|
|
730
|
+
var stage: String = " ".join(words.slice(0, words.size() - 1))
|
|
731
|
+
marks.append([stage, words[words.size() - 1].to_int()])
|
|
732
|
+
return marks
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
## How far a client started at [param started_ms] got by the marks in [param progress], said for a
|
|
736
|
+
## caller whose read it stopped: the time went to starting PowerShell, loading UI Automation, or the
|
|
737
|
+
## walks, and which tells a loaded machine from a window that never answers.
|
|
738
|
+
static func _reached(progress: String, started_ms: int) -> String:
|
|
739
|
+
var marks: Array = _marks(progress)
|
|
740
|
+
var at: Dictionary = {}
|
|
741
|
+
var walks: int = 0
|
|
742
|
+
var last: String = ""
|
|
743
|
+
for mark: Array in marks:
|
|
744
|
+
var stage: String = mark[0]
|
|
745
|
+
if stage.begins_with("walked "):
|
|
746
|
+
walks += 1
|
|
747
|
+
last = stage.trim_prefix("walked ")
|
|
748
|
+
elif not at.has(stage):
|
|
749
|
+
at[stage] = mark[1]
|
|
750
|
+
if not at.has("began"):
|
|
751
|
+
return "PowerShell had not begun the client"
|
|
752
|
+
var began: int = at["began"] - started_ms
|
|
753
|
+
if not at.has("loaded"):
|
|
754
|
+
return "PowerShell began the client after %d ms and was still loading UI Automation" % began
|
|
755
|
+
var loaded: int = at["loaded"] - at["began"]
|
|
756
|
+
if walks == 0:
|
|
757
|
+
return (
|
|
758
|
+
(
|
|
759
|
+
"PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the"
|
|
760
|
+
+ " first walk of the windows had not ended"
|
|
761
|
+
)
|
|
762
|
+
% [began, loaded]
|
|
763
|
+
)
|
|
764
|
+
return (
|
|
765
|
+
(
|
|
766
|
+
"PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the windows"
|
|
767
|
+
+ " were walked %d time%s, the last finding %s element%s"
|
|
768
|
+
)
|
|
769
|
+
% [began, loaded, walks, "" if walks == 1 else "s", last, "" if last == "1" else "s"]
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
## Where the time of a client started at [param started_ms] and done at [param done_ms] went, by
|
|
774
|
+
## the marks in [param progress]: starting PowerShell, loading UI Automation, and walking the
|
|
775
|
+
## windows. Empty when the client marked nothing.
|
|
776
|
+
static func _took(progress: String, started_ms: int, done_ms: int) -> Dictionary:
|
|
777
|
+
var at: Dictionary = {}
|
|
778
|
+
for mark: Array in _marks(progress):
|
|
779
|
+
var stage: String = mark[0]
|
|
780
|
+
if not at.has(stage):
|
|
781
|
+
at[stage] = mark[1]
|
|
782
|
+
if not at.has("began") or not at.has("loaded"):
|
|
783
|
+
return {}
|
|
784
|
+
return {
|
|
785
|
+
"start": at["began"] - started_ms,
|
|
786
|
+
"load": at["loaded"] - at["began"],
|
|
787
|
+
"walk": done_ms - at["loaded"],
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
667
791
|
static func _refused(message: String) -> Dictionary:
|
|
668
792
|
return {"type": "error", "message": message}
|
|
@@ -25,6 +25,9 @@ const ROWS: String = "--rows-in"
|
|
|
25
25
|
## that is not parameterised. gdUnit4 prints nothing for a case it skips, and the caller needs every
|
|
26
26
|
## row an engine found to tell one that no engine ran.
|
|
27
27
|
const LEFT_OUT: String = "gdharness left out"
|
|
28
|
+
## Printed before each row an entry of [constant SKIPPING] leaves out, as
|
|
29
|
+
## `res://suite.gd > test_case:row`, so the caller can tell an entry naming a row that does not exist.
|
|
30
|
+
const IGNORED_ROW: String = "gdharness ignored"
|
|
28
31
|
## Printed after a case or a suite's hooks that gdUnit4 says left orphan nodes, as
|
|
29
32
|
## `res://suite.gd > test_case left 2: Node "Sandbox" of res://sandbox.gd, Timer "Timer"`, with
|
|
30
33
|
## `after` for the suite's hooks. gdUnit4 names them only where a case calls
|
|
@@ -92,6 +95,8 @@ class GdhRunner:
|
|
|
92
95
|
## base, so an inherited case left out under its suite ran in every engine the suite was dealt to,
|
|
93
96
|
## and a case of `res://tests/a_test.gd` was also left out of `res://more/tests/a_test.gd`.
|
|
94
97
|
var left_out: Dictionary[String, bool] = {}
|
|
98
|
+
## The rows of parameterised cases to leave out, by `res://suite.gd:test_case:row`.
|
|
99
|
+
var left_out_rows: Dictionary[String, bool] = {}
|
|
95
100
|
## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups against.
|
|
96
101
|
var cases_ended: int = 0
|
|
97
102
|
## The marker of the shutdown walk's oldest end, made as the run starts.
|
|
@@ -164,6 +169,15 @@ class GdhRunner:
|
|
|
164
169
|
func is_skipped(test: GdUnitTestCase) -> bool:
|
|
165
170
|
if left_out.has("%s:%s" % [test.suite_resource_path, test.test_name]) or super(test):
|
|
166
171
|
return true
|
|
172
|
+
var row: String = "%s:%s:%d" % [test.suite_resource_path, test.test_name, test.attribute_index]
|
|
173
|
+
if test.attribute_index >= 0 and left_out_rows.has(row):
|
|
174
|
+
print(
|
|
175
|
+
(
|
|
176
|
+
"%s %s > %s:%d"
|
|
177
|
+
% [IGNORED_ROW, test.suite_resource_path, test.test_name, test.attribute_index]
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return true
|
|
167
181
|
var share: GdhRowShare = row_shares.get("%s:%s" % [test.suite_resource_path, test.test_name], null)
|
|
168
182
|
if share == null or not share.leaves_out(test.attribute_index):
|
|
169
183
|
return false
|
|
@@ -476,7 +490,9 @@ static func started(given: PackedStringArray) -> GdhRunner:
|
|
|
476
490
|
write_expected(expected_out, suites)
|
|
477
491
|
runner.add_test_suites(suites)
|
|
478
492
|
for entry: String in listed(asked, SKIPPING):
|
|
479
|
-
if
|
|
493
|
+
if is_one_row(entry):
|
|
494
|
+
runner.left_out_rows[entry] = true
|
|
495
|
+
elif is_one_case(entry):
|
|
480
496
|
runner.left_out[entry] = true
|
|
481
497
|
else:
|
|
482
498
|
runner.skip_test_suite(entry)
|
|
@@ -560,6 +576,13 @@ static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
|
|
|
560
576
|
## Whether [param entry] of a [constant SKIPPING] file names one case of one suite file, as
|
|
561
577
|
## `res://suite.gd:test_case`, rather than a suite or a case by gdUnit4's looser names, which go to
|
|
562
578
|
## gdUnit4 as they are.
|
|
579
|
+
## Whether [param entry] names one row of a parameterised case, as `res://suite.gd:test_case:row`.
|
|
580
|
+
static func is_one_row(entry: String) -> bool:
|
|
581
|
+
var at: int = entry.rfind(":")
|
|
582
|
+
var row: String = entry.substr(at + 1)
|
|
583
|
+
return at > 0 and row.is_valid_int() and not row.begins_with("-") and is_one_case(entry.substr(0, at))
|
|
584
|
+
|
|
585
|
+
|
|
563
586
|
static func is_one_case(entry: String) -> bool:
|
|
564
587
|
var at: int = entry.rfind(":")
|
|
565
588
|
return (
|
package/build/keeper.js
CHANGED
|
@@ -8297,6 +8297,7 @@ function entryFor(shape, launch, held) {
|
|
|
8297
8297
|
}
|
|
8298
8298
|
|
|
8299
8299
|
// src/engine-leaks.ts
|
|
8300
|
+
var REFCOUNTED_HELD = "A RefCounted alive at exit is held by a cycle of references (two holding each other, or one " + "holding a lambda that captures it) or by an Object never freed, rather than by a node left out " + "of the tree; a WeakRef on one side of the cycle, or clearing the reference once done, frees it.";
|
|
8300
8301
|
var WALK_STOPPED_SENTENCE = "The engine's shutdown stopped part way through its scripts (godot#117991, fixed in 4.8 by " + "godot#120976), so it reported as leaked at exit what it never got to free, and the report is the " + "engine's rather than the run's. gdharness holds every script loaded from a file past that walk, " + "which keeps it whole, so this one stopped on a script it could not hold, such as one made in code. " + "A leak of the run's own is not told apart in such a run.";
|
|
8301
8302
|
var NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*";
|
|
8302
8303
|
var EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.32",
|
|
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",
|