gdharness 1.3.22 → 1.3.24
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
|
@@ -9786,6 +9786,17 @@ function scriptsNaming(projectPath, classes) {
|
|
|
9786
9786
|
});
|
|
9787
9787
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
9788
9788
|
}
|
|
9789
|
+
function scriptsDeclaring(projectPath, classes) {
|
|
9790
|
+
const named = new Set(classes);
|
|
9791
|
+
const declaring = [];
|
|
9792
|
+
eachScript(projectPath, (script, source) => {
|
|
9793
|
+
const declares = DECLARATION.exec(source)?.[1];
|
|
9794
|
+
if (declares !== undefined && named.has(declares)) {
|
|
9795
|
+
declaring.push({ script, tool: TOOL.test(codeOf(source)) });
|
|
9796
|
+
}
|
|
9797
|
+
});
|
|
9798
|
+
return declaring.sort((a, b) => a.script.localeCompare(b.script));
|
|
9799
|
+
}
|
|
9789
9800
|
function cachedClasses(projectPath) {
|
|
9790
9801
|
const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
9791
9802
|
if (!existsSync4(cache)) {
|
|
@@ -10201,7 +10212,7 @@ var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scr
|
|
|
10201
10212
|
var init_engine_leaks = __esm(() => {
|
|
10202
10213
|
init_class_cache();
|
|
10203
10214
|
init_gdscript_source();
|
|
10204
|
-
EXIT_LEAK = /ObjectDB instances? (?:was |were )?leaked at exit|resources? still in use at exit|Pages in use exist at exit/;
|
|
10215
|
+
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/;
|
|
10205
10216
|
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.";
|
|
10206
10217
|
EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
|
|
10207
10218
|
});
|
|
@@ -10629,8 +10640,9 @@ function orphansLeftOn(line) {
|
|
|
10629
10640
|
}
|
|
10630
10641
|
function orphansLeftSaid(left) {
|
|
10631
10642
|
const nodes = left.count === 1 ? "1 node" : `${String(left.count)} nodes`;
|
|
10632
|
-
const
|
|
10633
|
-
|
|
10643
|
+
const hook = HOOKS_LEFT.get(left.during);
|
|
10644
|
+
const where = hook === undefined ? `${left.suite} > ${left.during}` : `${left.suite}'s ${hook}`;
|
|
10645
|
+
return `${where} left ${nodes} neither freed nor in the tree, which gdUnit4 counts as orphans: ${left.nodes}. Free each one, or make it with auto_free(), which frees it once the ${hook === undefined ? "case" : "suite"} is over.`;
|
|
10634
10646
|
}
|
|
10635
10647
|
function orphansPrinted(printed) {
|
|
10636
10648
|
const suites = [];
|
|
@@ -10712,7 +10724,7 @@ function testProgressMessage(printed, elapsedMs) {
|
|
|
10712
10724
|
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`}`;
|
|
10713
10725
|
return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
|
|
10714
10726
|
}
|
|
10715
|
-
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, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
|
|
10727
|
+
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;
|
|
10716
10728
|
var init_junit = __esm(() => {
|
|
10717
10729
|
MalformedReportError = class MalformedReportError extends Error {
|
|
10718
10730
|
constructor(message) {
|
|
@@ -10743,6 +10755,11 @@ var init_junit = __esm(() => {
|
|
|
10743
10755
|
};
|
|
10744
10756
|
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
10745
10757
|
ORPHANS_LEFT = /^gdharness orphans: (res:\/\/.+?\.(?:gd|cs)) > (.+?) left (\d+): (.+?)\s*$/;
|
|
10758
|
+
HOOKS_LEFT = new Map([
|
|
10759
|
+
["before", "before()"],
|
|
10760
|
+
["after", "after()"],
|
|
10761
|
+
["before or after", "before() or after()"]
|
|
10762
|
+
]);
|
|
10746
10763
|
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
10747
10764
|
ORPHANS = /(\d+)\s+orphans/;
|
|
10748
10765
|
SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
|
|
@@ -16462,7 +16479,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16462
16479
|
title: "Run tests",
|
|
16463
16480
|
effect: "additive",
|
|
16464
16481
|
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. 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.",
|
|
16465
|
-
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 or
|
|
16482
|
+
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,
|
|
16466
16483
|
parameters: {
|
|
16467
16484
|
projectPath: PROJECT_PATH,
|
|
16468
16485
|
path: {
|
|
@@ -55863,6 +55880,11 @@ ${error.text}`;
|
|
|
55863
55880
|
});
|
|
55864
55881
|
}
|
|
55865
55882
|
async reloadDependents(projectPath, classes) {
|
|
55883
|
+
for (const { script, tool } of scriptsDeclaring(projectPath, classes)) {
|
|
55884
|
+
if (!tool) {
|
|
55885
|
+
await this.reloadOnce(script);
|
|
55886
|
+
}
|
|
55887
|
+
}
|
|
55866
55888
|
return await this.reloadInEditor(scriptsNaming(projectPath, classes), projectPath);
|
|
55867
55889
|
}
|
|
55868
55890
|
async reloadInEditor(scripts, projectPath = "") {
|
|
@@ -56,7 +56,15 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
56
56
|
if not ResourceLoader.exists(path):
|
|
57
57
|
return {"ok": false, "error": "No script at " + path}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
# The copy the editor holds, when it holds one, and otherwise a load that reads the file, rather
|
|
60
|
+
# than load(): a load recompiles a script whose base has been reloaded from the text it holds,
|
|
61
|
+
# which is the text before the file changed. A rename reloading a base and then its subclass
|
|
62
|
+
# logged "not found in base" for the subclass's old call to the renamed method, a moment before
|
|
63
|
+
# the reload below compiled it clean.
|
|
64
|
+
var cached: Resource = ResourceLoader.get_cached_ref(path)
|
|
65
|
+
var loaded: Resource = (
|
|
66
|
+
cached if cached != null else ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
67
|
+
)
|
|
60
68
|
var script: GDScript = loaded as GDScript
|
|
61
69
|
if script == null:
|
|
62
70
|
return {"ok": false, "error": path + " is not a GDScript"}
|
|
@@ -101,6 +101,10 @@ class GdhRunner:
|
|
|
101
101
|
var orphans_at_case: Dictionary[int, bool] = {}
|
|
102
102
|
var orphans_at_suite: Dictionary[int, bool] = {}
|
|
103
103
|
var orphans_named: Dictionary[int, bool] = {}
|
|
104
|
+
## The orphans new as the suite's first case began, which its before() made, and whether a case
|
|
105
|
+
## has begun: gdUnit4 counts both hooks' orphans at the suite's end.
|
|
106
|
+
var orphans_from_before: Dictionary[int, bool] = {}
|
|
107
|
+
var case_began: bool = false
|
|
104
108
|
## The suite running, which a case's own event does not carry.
|
|
105
109
|
var suite_running: String = ""
|
|
106
110
|
|
|
@@ -177,37 +181,54 @@ class GdhRunner:
|
|
|
177
181
|
suite_running = event.resource_path()
|
|
178
182
|
orphans_at_suite = orphan_ids()
|
|
179
183
|
orphans_named.clear()
|
|
184
|
+
orphans_from_before.clear()
|
|
185
|
+
case_began = false
|
|
180
186
|
GdUnitEvent.TESTCASE_BEFORE:
|
|
181
187
|
orphans_at_case = orphan_ids()
|
|
188
|
+
if not case_began:
|
|
189
|
+
case_began = true
|
|
190
|
+
for id: int in new_orphans(orphans_at_suite):
|
|
191
|
+
orphans_from_before[id] = true
|
|
182
192
|
GdUnitEvent.TESTCASE_AFTER:
|
|
183
193
|
cases_ended += 1
|
|
184
194
|
super(event)
|
|
185
195
|
# After gdUnit4's own report of the case, so the line falls under it.
|
|
196
|
+
if event.orphan_nodes() == 0:
|
|
197
|
+
return
|
|
186
198
|
if event.type() == GdUnitEvent.TESTCASE_AFTER:
|
|
187
|
-
|
|
199
|
+
say_left(event.test_name(), new_orphans(orphans_at_case))
|
|
188
200
|
elif event.type() == GdUnitEvent.TESTSUITE_AFTER:
|
|
189
|
-
|
|
201
|
+
var left: Array[int] = new_orphans(orphans_at_suite)
|
|
202
|
+
if not case_began:
|
|
203
|
+
say_left("before or after", left)
|
|
204
|
+
return
|
|
205
|
+
say_left("before", left.filter(func(id: int) -> bool: return orphans_from_before.has(id)))
|
|
206
|
+
say_left("after", left.filter(func(id: int) -> bool: return not orphans_from_before.has(id)))
|
|
207
|
+
|
|
208
|
+
## The orphans now that were not there in [param before] and no line has named yet.
|
|
209
|
+
func new_orphans(before: Dictionary[int, bool]) -> Array[int]:
|
|
210
|
+
var found: Array[int] = []
|
|
211
|
+
for id: int in orphan_ids():
|
|
212
|
+
if not before.has(id) and not orphans_named.has(id):
|
|
213
|
+
found.append(id)
|
|
214
|
+
return found
|
|
190
215
|
|
|
191
|
-
## Prints the orphans [param
|
|
192
|
-
##
|
|
193
|
-
func
|
|
194
|
-
if
|
|
216
|
+
## Prints the orphans [param ids] as left by [param during], under [constant ORPHANS_LEFT], once
|
|
217
|
+
## each in a suite.
|
|
218
|
+
func say_left(during: String, ids: Array[int]) -> void:
|
|
219
|
+
if ids.is_empty():
|
|
195
220
|
return
|
|
196
221
|
var named: PackedStringArray = []
|
|
197
|
-
|
|
198
|
-
for id: int in orphan_ids():
|
|
199
|
-
var node: Node = instance_from_id(id) as Node
|
|
200
|
-
if node == null or before.has(id) or orphans_named.has(id):
|
|
201
|
-
continue
|
|
222
|
+
for id: int in ids:
|
|
202
223
|
orphans_named[id] = true
|
|
203
|
-
count += 1
|
|
204
224
|
if named.size() < ORPHANS_NAMED:
|
|
205
|
-
var _named: bool = named.append(described(
|
|
206
|
-
if
|
|
207
|
-
return
|
|
208
|
-
var more: String = "" if count <= named.size() else ", and %d more" % (count - named.size())
|
|
225
|
+
var _named: bool = named.append(described(instance_from_id(id) as Node))
|
|
226
|
+
var more: String = "" if ids.size() <= named.size() else ", and %d more" % (ids.size() - named.size())
|
|
209
227
|
print(
|
|
210
|
-
|
|
228
|
+
(
|
|
229
|
+
"%s %s > %s left %d: %s%s"
|
|
230
|
+
% [ORPHANS_LEFT, suite_running, during, ids.size(), ", ".join(named), more]
|
|
231
|
+
)
|
|
211
232
|
)
|
|
212
233
|
|
|
213
234
|
## The nodes outside every tree that are not on their way out, by instance id, as gdUnit4 counts
|
package/build/keeper.js
CHANGED
|
@@ -8308,6 +8308,11 @@ var IGNORING_CASE = " \\(ignoring case\\)";
|
|
|
8308
8308
|
var PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
8309
8309
|
var QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
8310
8310
|
var COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
8311
|
+
var HOOKS_LEFT = new Map([
|
|
8312
|
+
["before", "before()"],
|
|
8313
|
+
["after", "after()"],
|
|
8314
|
+
["before or after", "before() or after()"]
|
|
8315
|
+
]);
|
|
8311
8316
|
|
|
8312
8317
|
// src/suite-judge.ts
|
|
8313
8318
|
var TIME = String.raw`(?:\d+h )?(?:\d+min )?(?:\d+s )?\d+ms`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.24",
|
|
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",
|