gdharness 1.2.1 → 1.2.3
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 +39 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +61 -12
- package/build/index.js +39 -10
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17977,7 +17977,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17977
17977
|
},
|
|
17978
17978
|
{
|
|
17979
17979
|
name: "project_test",
|
|
17980
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17980
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, every report 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. 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 rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
17981
17981
|
parameters: {
|
|
17982
17982
|
projectPath: PROJECT_PATH,
|
|
17983
17983
|
path: {
|
|
@@ -39250,18 +39250,25 @@ function count(attributes, name) {
|
|
|
39250
39250
|
return Number.isFinite(value) ? value : 0;
|
|
39251
39251
|
}
|
|
39252
39252
|
function caseOf(suite, element) {
|
|
39253
|
-
const
|
|
39254
|
-
|
|
39255
|
-
|
|
39253
|
+
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
39254
|
+
const detail = child.text.trim();
|
|
39255
|
+
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
39256
|
+
});
|
|
39257
|
+
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
39258
|
+
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
39256
39259
|
return {
|
|
39257
39260
|
suite: suite.attributes["name"] ?? "",
|
|
39258
39261
|
name: element.attributes["name"] ?? "",
|
|
39259
39262
|
status,
|
|
39260
39263
|
time: count(element.attributes, "time"),
|
|
39261
|
-
message:
|
|
39262
|
-
detail: detail
|
|
39264
|
+
message: reports[0]?.message ?? null,
|
|
39265
|
+
detail: reports[0]?.detail ?? null,
|
|
39266
|
+
reports
|
|
39263
39267
|
};
|
|
39264
39268
|
}
|
|
39269
|
+
function failedAnAssertion(entry) {
|
|
39270
|
+
return entry.reports.some((report) => report.kind === "failure");
|
|
39271
|
+
}
|
|
39265
39272
|
function pathOf(suite) {
|
|
39266
39273
|
const directory = suite.attributes["package"];
|
|
39267
39274
|
const name = suite.attributes["name"];
|
|
@@ -39276,7 +39283,7 @@ function parseJUnit(xml) {
|
|
|
39276
39283
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
39277
39284
|
const suites = suiteElements.map((suite) => {
|
|
39278
39285
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
39279
|
-
const failures = cases.filter(
|
|
39286
|
+
const failures = cases.filter(failedAnAssertion).length;
|
|
39280
39287
|
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
39281
39288
|
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
39282
39289
|
return {
|
|
@@ -39296,7 +39303,7 @@ function parseJUnit(xml) {
|
|
|
39296
39303
|
const all = suites.flatMap((suite) => suite.cases);
|
|
39297
39304
|
return {
|
|
39298
39305
|
tests: all.length,
|
|
39299
|
-
failures: all.filter(
|
|
39306
|
+
failures: all.filter(failedAnAssertion).length,
|
|
39300
39307
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
39301
39308
|
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
39302
39309
|
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
@@ -39418,6 +39425,25 @@ ${note}`;
|
|
|
39418
39425
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
39419
39426
|
});
|
|
39420
39427
|
}
|
|
39428
|
+
function failedCases(report, printed) {
|
|
39429
|
+
const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
|
|
39430
|
+
const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
|
|
39431
|
+
let next = 0;
|
|
39432
|
+
return cases.map(({ entry, path }) => {
|
|
39433
|
+
const reports = read.slice(next, next + entry.reports.length);
|
|
39434
|
+
next += entry.reports.length;
|
|
39435
|
+
return {
|
|
39436
|
+
suite: entry.suite,
|
|
39437
|
+
name: entry.name,
|
|
39438
|
+
status: entry.status,
|
|
39439
|
+
time: entry.time,
|
|
39440
|
+
path,
|
|
39441
|
+
message: reports[0]?.message ?? null,
|
|
39442
|
+
detail: reports[0]?.detail ?? null,
|
|
39443
|
+
...reports.length > 1 ? { reports } : {}
|
|
39444
|
+
};
|
|
39445
|
+
});
|
|
39446
|
+
}
|
|
39421
39447
|
function masked(expected, actual) {
|
|
39422
39448
|
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
39423
39449
|
return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
|
|
@@ -42840,6 +42866,9 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
|
42840
42866
|
function noCodeWillCome(run) {
|
|
42841
42867
|
return !exited(run);
|
|
42842
42868
|
}
|
|
42869
|
+
function transcriptNote(transcript, going) {
|
|
42870
|
+
return going ? `Everything this run has printed is in ${transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.` : `Everything this run printed is in ${transcript}, uncapped and complete, since the run is over.`;
|
|
42871
|
+
}
|
|
42843
42872
|
function stopWaitMs() {
|
|
42844
42873
|
return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
|
|
42845
42874
|
}
|
|
@@ -45432,7 +45461,7 @@ class GodotServer {
|
|
|
45432
45461
|
isError: true
|
|
45433
45462
|
};
|
|
45434
45463
|
}
|
|
45435
|
-
const failed =
|
|
45464
|
+
const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
|
|
45436
45465
|
const orphans = orphansPrinted(said);
|
|
45437
45466
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
45438
45467
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -47575,7 +47604,7 @@ class GodotServer {
|
|
|
47575
47604
|
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
47576
47605
|
}
|
|
47577
47606
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
47578
|
-
notes.push(
|
|
47607
|
+
notes.push(transcriptNote(run.transcript, going));
|
|
47579
47608
|
}
|
|
47580
47609
|
return this.jsonTextResponse({
|
|
47581
47610
|
running: going,
|
|
@@ -377,10 +377,15 @@ static func drawn_over(target: Control) -> Node:
|
|
|
377
377
|
|
|
378
378
|
|
|
379
379
|
## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
|
|
380
|
-
## which nodes hide it, nearest first, and what
|
|
381
|
-
##
|
|
382
|
-
##
|
|
383
|
-
##
|
|
380
|
+
## which nodes hide it, nearest first, and what has taken its place. Reading visible off the node
|
|
381
|
+
## itself says true when only an ancestor is hidden, so without this a caller had nothing to unhide;
|
|
382
|
+
## every hidden one is named because unhiding only the nearest leaves the rest hiding it.
|
|
383
|
+
##
|
|
384
|
+
## What has taken its place is what a click at its centre reaches now, which is the screen a game
|
|
385
|
+
## shows instead. Without a control there, as for a 3D node, it is what is shown beside the
|
|
386
|
+
## outermost hidden node: a game hides a screen by hiding the screen's root, and the nearest hidden
|
|
387
|
+
## node can be a panel deep inside it whose siblings are hidden with it. "" when nothing on the way
|
|
388
|
+
## up says the node is hidden.
|
|
384
389
|
static func why_hidden(node: Node) -> String:
|
|
385
390
|
var hiding: Array[Node] = _hiding(node)
|
|
386
391
|
if hiding.is_empty():
|
|
@@ -391,27 +396,71 @@ static func why_hidden(node: Node) -> String:
|
|
|
391
396
|
named.append("its own visible is false")
|
|
392
397
|
else:
|
|
393
398
|
named.append("%s is hidden%s" % [one.get_path(), "" if named.is_empty() else " too"])
|
|
399
|
+
var control: Control = node as Control
|
|
400
|
+
var instead: Node = null if control == null else drawn_at(control)
|
|
401
|
+
if instead != null:
|
|
402
|
+
return " (%s; %s is drawn where it would be)" % [", and ".join(named), instead.get_path()]
|
|
403
|
+
var outermost: Node = hiding.back()
|
|
394
404
|
var beside: Array[String] = []
|
|
395
|
-
var parent: Node =
|
|
405
|
+
var parent: Node = outermost.get_parent()
|
|
396
406
|
if parent != null:
|
|
397
407
|
for sibling: Node in parent.get_children():
|
|
398
|
-
if sibling !=
|
|
408
|
+
if sibling != outermost and _shown(sibling):
|
|
399
409
|
beside.append(str(sibling.get_path()))
|
|
400
|
-
return " (%s%s)" % [", and ".join(named), _shown_beside(beside)]
|
|
410
|
+
return " (%s%s)" % [", and ".join(named), _shown_beside(beside, outermost)]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
## What a click at the centre of [param target], which is hidden, would reach instead, or null.
|
|
414
|
+
##
|
|
415
|
+
## The topmost thing there that takes the pointer, whatever order it is drawn in relative to the
|
|
416
|
+
## target, since the target is not drawn at all; and never a node holding the target, which covers
|
|
417
|
+
## its place by containing it and is not what took it.
|
|
418
|
+
static func drawn_at(target: Control) -> Node:
|
|
419
|
+
var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
|
|
420
|
+
var point: Vector2 = _centre_of(target)
|
|
421
|
+
var none: Dictionary[ScrollContainer, Vector2] = {}
|
|
422
|
+
var viewport: Viewport = target.get_viewport()
|
|
423
|
+
var best: Control = null
|
|
424
|
+
for each: Node in drawn:
|
|
425
|
+
var window: Window = each as Window
|
|
426
|
+
if window != null:
|
|
427
|
+
if _window_over(target, point, window):
|
|
428
|
+
return window
|
|
429
|
+
continue
|
|
430
|
+
var control: Control = each
|
|
431
|
+
if control.is_ancestor_of(target) or target.is_ancestor_of(control):
|
|
432
|
+
continue
|
|
433
|
+
if control.get_viewport() != viewport or not _holds(control, point):
|
|
434
|
+
continue
|
|
435
|
+
if _clipped_away(control, point, none):
|
|
436
|
+
continue
|
|
437
|
+
if best == null or _drawn_above(control, best):
|
|
438
|
+
best = control
|
|
439
|
+
return best
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
## Whether [param control] is drawn above [param other]: on a higher canvas layer, or later on the
|
|
443
|
+
## same one.
|
|
444
|
+
static func _drawn_above(control: Control, other: Control) -> bool:
|
|
445
|
+
var layer: int = _layer_of(control)
|
|
446
|
+
var other_layer: int = _layer_of(other)
|
|
447
|
+
return layer > other_layer or (layer == other_layer and control.is_greater_than(other))
|
|
401
448
|
|
|
402
449
|
|
|
403
|
-
## "; A, B and C
|
|
404
|
-
|
|
450
|
+
## "; A, B and C are shown beside N" for [param beside] and the hidden node [param hidden], naming
|
|
451
|
+
## three at most and counting the rest.
|
|
452
|
+
static func _shown_beside(beside: Array[String], hidden: Node) -> String:
|
|
405
453
|
if beside.is_empty():
|
|
406
454
|
return ""
|
|
455
|
+
var at: String = str(hidden.get_path())
|
|
407
456
|
if beside.size() == 1:
|
|
408
|
-
return "; %s
|
|
457
|
+
return "; %s is shown beside %s" % [beside[0], at]
|
|
409
458
|
var listed: Array[String] = beside.slice(0, 3)
|
|
410
459
|
var rest: int = beside.size() - listed.size()
|
|
411
460
|
if rest > 0:
|
|
412
|
-
return "; %s and %d more
|
|
461
|
+
return "; %s and %d more are shown beside %s" % [", ".join(listed), rest, at]
|
|
413
462
|
var last: String = listed.pop_back()
|
|
414
|
-
return "; %s and %s
|
|
463
|
+
return "; %s and %s are shown beside %s" % [", ".join(listed), last, at]
|
|
415
464
|
|
|
416
465
|
|
|
417
466
|
## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
|
package/build/index.js
CHANGED
|
@@ -29817,18 +29817,25 @@ function count(attributes, name) {
|
|
|
29817
29817
|
return Number.isFinite(value) ? value : 0;
|
|
29818
29818
|
}
|
|
29819
29819
|
function caseOf(suite, element) {
|
|
29820
|
-
const
|
|
29821
|
-
|
|
29822
|
-
|
|
29820
|
+
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
29821
|
+
const detail = child.text.trim();
|
|
29822
|
+
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
29823
|
+
});
|
|
29824
|
+
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
29825
|
+
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
29823
29826
|
return {
|
|
29824
29827
|
suite: suite.attributes["name"] ?? "",
|
|
29825
29828
|
name: element.attributes["name"] ?? "",
|
|
29826
29829
|
status,
|
|
29827
29830
|
time: count(element.attributes, "time"),
|
|
29828
|
-
message:
|
|
29829
|
-
detail: detail
|
|
29831
|
+
message: reports[0]?.message ?? null,
|
|
29832
|
+
detail: reports[0]?.detail ?? null,
|
|
29833
|
+
reports
|
|
29830
29834
|
};
|
|
29831
29835
|
}
|
|
29836
|
+
function failedAnAssertion(entry) {
|
|
29837
|
+
return entry.reports.some((report) => report.kind === "failure");
|
|
29838
|
+
}
|
|
29832
29839
|
function pathOf(suite) {
|
|
29833
29840
|
const directory = suite.attributes["package"];
|
|
29834
29841
|
const name = suite.attributes["name"];
|
|
@@ -29843,7 +29850,7 @@ function parseJUnit(xml) {
|
|
|
29843
29850
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
29844
29851
|
const suites = suiteElements.map((suite) => {
|
|
29845
29852
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
29846
|
-
const failures = cases.filter(
|
|
29853
|
+
const failures = cases.filter(failedAnAssertion).length;
|
|
29847
29854
|
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
29848
29855
|
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
29849
29856
|
return {
|
|
@@ -29863,7 +29870,7 @@ function parseJUnit(xml) {
|
|
|
29863
29870
|
const all = suites.flatMap((suite) => suite.cases);
|
|
29864
29871
|
return {
|
|
29865
29872
|
tests: all.length,
|
|
29866
|
-
failures: all.filter(
|
|
29873
|
+
failures: all.filter(failedAnAssertion).length,
|
|
29867
29874
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
29868
29875
|
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
29869
29876
|
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
@@ -30010,6 +30017,25 @@ ${note}`;
|
|
|
30010
30017
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
30011
30018
|
});
|
|
30012
30019
|
}
|
|
30020
|
+
function failedCases(report, printed) {
|
|
30021
|
+
const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
|
|
30022
|
+
const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
|
|
30023
|
+
let next = 0;
|
|
30024
|
+
return cases.map(({ entry, path }) => {
|
|
30025
|
+
const reports = read.slice(next, next + entry.reports.length);
|
|
30026
|
+
next += entry.reports.length;
|
|
30027
|
+
return {
|
|
30028
|
+
suite: entry.suite,
|
|
30029
|
+
name: entry.name,
|
|
30030
|
+
status: entry.status,
|
|
30031
|
+
time: entry.time,
|
|
30032
|
+
path,
|
|
30033
|
+
message: reports[0]?.message ?? null,
|
|
30034
|
+
detail: reports[0]?.detail ?? null,
|
|
30035
|
+
...reports.length > 1 ? { reports } : {}
|
|
30036
|
+
};
|
|
30037
|
+
});
|
|
30038
|
+
}
|
|
30013
30039
|
var MASKED_BRACKET = "[lb]";
|
|
30014
30040
|
function masked(expected, actual) {
|
|
30015
30041
|
const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
|
|
@@ -34054,7 +34080,7 @@ var TOOL_SPECS = [
|
|
|
34054
34080
|
},
|
|
34055
34081
|
{
|
|
34056
34082
|
name: "project_test",
|
|
34057
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
34083
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, every report 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. 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 rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
34058
34084
|
parameters: {
|
|
34059
34085
|
projectPath: PROJECT_PATH,
|
|
34060
34086
|
path: {
|
|
@@ -35268,6 +35294,9 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
|
|
|
35268
35294
|
function noCodeWillCome(run) {
|
|
35269
35295
|
return !exited(run);
|
|
35270
35296
|
}
|
|
35297
|
+
function transcriptNote(transcript, going) {
|
|
35298
|
+
return going ? `Everything this run has printed is in ${transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.` : `Everything this run printed is in ${transcript}, uncapped and complete, since the run is over.`;
|
|
35299
|
+
}
|
|
35271
35300
|
function stopWaitMs() {
|
|
35272
35301
|
return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
|
|
35273
35302
|
}
|
|
@@ -37929,7 +37958,7 @@ class GodotServer {
|
|
|
37929
37958
|
isError: true
|
|
37930
37959
|
};
|
|
37931
37960
|
}
|
|
37932
|
-
const failed =
|
|
37961
|
+
const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
|
|
37933
37962
|
const orphans = orphansPrinted(said);
|
|
37934
37963
|
const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
|
|
37935
37964
|
const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
|
|
@@ -40072,7 +40101,7 @@ class GodotServer {
|
|
|
40072
40101
|
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
40073
40102
|
}
|
|
40074
40103
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
40075
|
-
notes.push(
|
|
40104
|
+
notes.push(transcriptNote(run.transcript, going));
|
|
40076
40105
|
}
|
|
40077
40106
|
return this.jsonTextResponse({
|
|
40078
40107
|
running: going,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
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",
|