gdharness 1.3.29 → 1.3.31
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 +162 -20
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -10176,6 +10176,36 @@ function walkStoppedPartWay(lines) {
|
|
|
10176
10176
|
}
|
|
10177
10177
|
return began;
|
|
10178
10178
|
}
|
|
10179
|
+
function heldAtExit(lines) {
|
|
10180
|
+
const objects = new Map;
|
|
10181
|
+
const inUse = [];
|
|
10182
|
+
for (const line of lines) {
|
|
10183
|
+
const object = LEAKED_INSTANCE.exec(line.trim())?.[1];
|
|
10184
|
+
if (object !== undefined) {
|
|
10185
|
+
objects.set(object, (objects.get(object) ?? 0) + 1);
|
|
10186
|
+
}
|
|
10187
|
+
const resource = STILL_IN_USE.exec(line.trim())?.[1];
|
|
10188
|
+
if (resource !== undefined) {
|
|
10189
|
+
inUse.push(resource);
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
10192
|
+
if (objects.size === 0 && inUse.length === 0) {
|
|
10193
|
+
return;
|
|
10194
|
+
}
|
|
10195
|
+
const counted = [...objects].sort(([a, many], [b, more]) => more - many || a.localeCompare(b));
|
|
10196
|
+
const objectsSaid = counted.map(([name, many]) => `${String(many)} ${name}`);
|
|
10197
|
+
const ownFirst = [
|
|
10198
|
+
...inUse.filter((path) => !path.startsWith("res://addons/")),
|
|
10199
|
+
...inUse.filter((path) => path.startsWith("res://addons/"))
|
|
10200
|
+
];
|
|
10201
|
+
const named = ownFirst.slice(0, IN_USE_NAMED);
|
|
10202
|
+
const more = ownFirst.length - named.length;
|
|
10203
|
+
const parts = [
|
|
10204
|
+
objectsSaid.length === 0 ? undefined : `objects left alive: ${objectsSaid.join(", ")}`,
|
|
10205
|
+
named.length === 0 ? undefined : `resources still in use: ${named.join(", ")}${more > 0 ? `, and ${String(more)} more` : ""}`
|
|
10206
|
+
].filter((part) => part !== undefined);
|
|
10207
|
+
return `The engine named them: ${parts.join("; ")}.`;
|
|
10208
|
+
}
|
|
10179
10209
|
function selfTypedExports(sources) {
|
|
10180
10210
|
const found = [];
|
|
10181
10211
|
for (const [script, source] of sources) {
|
|
@@ -10208,11 +10238,13 @@ function selfTypedSentence(found) {
|
|
|
10208
10238
|
const one = found.length === 1;
|
|
10209
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.`;
|
|
10210
10240
|
}
|
|
10211
|
-
var EXIT_LEAK, WALK_BEGAN = "gdharness: the engine's shutdown began clearing scripts", WALK_ENDED = "gdharness: the engine's shutdown cleared every script", 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, WALK_STOPPED_SENTENCE, NAME2 = "[\\p{L}_][\\p{L}\\p{N}_]*", EXPORTED;
|
|
10212
10242
|
var init_engine_leaks = __esm(() => {
|
|
10213
10243
|
init_class_cache();
|
|
10214
10244
|
init_gdscript_source();
|
|
10215
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
|
+
LEAKED_INSTANCE = /^Leaked instance: (\w+):/;
|
|
10247
|
+
STILL_IN_USE = /^Resource still in use: (.+?) \(\w+\)$/;
|
|
10216
10248
|
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.";
|
|
10217
10249
|
EXPORTED = new RegExp(`@export(?!_(?:group|subgroup|category)\\b)\\w*(?:\\([^)]*\\))?\\s+(?:@${NAME2}(?:\\([^)]*\\))?\\s+)*` + `(?:static\\s+)?var\\s+(${NAME2})\\s*:\\s*(?:Array|Dictionary)\\s*\\[([^\\]]*)\\]`, "gu");
|
|
10218
10250
|
});
|
|
@@ -11104,9 +11136,11 @@ function judgeEngine(printed, handed, end) {
|
|
|
11104
11136
|
if (printed.leaks.length > 0 && walkStoppedPartWay(printed.lines)) {
|
|
11105
11137
|
notes.push(`${WALK_STOPPED_SENTENCE} It said: ${printed.leaks.join(" ")}`);
|
|
11106
11138
|
} else {
|
|
11139
|
+
const held = heldAtExit(printed.lines);
|
|
11107
11140
|
printed.leaks.forEach((leak, at) => {
|
|
11108
11141
|
const last = at === printed.leaks.length - 1;
|
|
11109
|
-
|
|
11142
|
+
const after = last ? [held, selfTyped].filter((part) => part !== undefined) : [];
|
|
11143
|
+
faults.push(`the engine exited holding what it should have freed: ${[leak, ...after].join(" ")}`);
|
|
11110
11144
|
});
|
|
11111
11145
|
}
|
|
11112
11146
|
const memoryExit = end.code !== null && MEMORY_EXITS.has(end.code >>> 0);
|
|
@@ -11734,12 +11768,9 @@ class EngineSlots {
|
|
|
11734
11768
|
try {
|
|
11735
11769
|
for (;; ) {
|
|
11736
11770
|
signal?.throwIfAborted();
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
this.held.add(lock);
|
|
11741
|
-
return lock;
|
|
11742
|
-
}
|
|
11771
|
+
const lock = await this.takeFree();
|
|
11772
|
+
if (lock !== null) {
|
|
11773
|
+
return lock;
|
|
11743
11774
|
}
|
|
11744
11775
|
mark ??= this.waitMarked();
|
|
11745
11776
|
const now = Date.now();
|
|
@@ -11760,6 +11791,17 @@ class EngineSlots {
|
|
|
11760
11791
|
}
|
|
11761
11792
|
}
|
|
11762
11793
|
}
|
|
11794
|
+
async takeFree() {
|
|
11795
|
+
mkdirSync3(this.directory, { recursive: true });
|
|
11796
|
+
for (let slot = 0;slot < this.count; slot += 1) {
|
|
11797
|
+
const lock = join8(this.directory, `${String(slot)}.lock`);
|
|
11798
|
+
if (this.made(lock) || await this.takenOver(lock)) {
|
|
11799
|
+
this.held.add(lock);
|
|
11800
|
+
return lock;
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11803
|
+
return null;
|
|
11804
|
+
}
|
|
11763
11805
|
waitMarked() {
|
|
11764
11806
|
const marks = join8(this.directory, WAITING_FOLDER);
|
|
11765
11807
|
waitsMarked += 1;
|
|
@@ -16606,7 +16648,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16606
16648
|
title: "Run tests",
|
|
16607
16649
|
effect: "additive",
|
|
16608
16650
|
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.",
|
|
16609
|
-
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,
|
|
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 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,
|
|
16610
16652
|
parameters: {
|
|
16611
16653
|
projectPath: PROJECT_PATH,
|
|
16612
16654
|
path: {
|
|
@@ -16627,7 +16669,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16627
16669
|
engineArgs: {
|
|
16628
16670
|
type: "array",
|
|
16629
16671
|
items: { type: "string" },
|
|
16630
|
-
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,
|
|
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, 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'.`
|
|
16631
16673
|
},
|
|
16632
16674
|
failFast: {
|
|
16633
16675
|
type: "boolean",
|
|
@@ -18638,7 +18680,7 @@ function dealt(units, declared, most) {
|
|
|
18638
18680
|
return [];
|
|
18639
18681
|
}
|
|
18640
18682
|
const windowed = units.filter((unit) => unit.windowed);
|
|
18641
|
-
const headless = units.filter((unit) => !unit.windowed).sort(
|
|
18683
|
+
const headless = units.filter((unit) => !unit.windowed).sort(slowestFirst);
|
|
18642
18684
|
const together = windowed.reduce((sum, unit) => sum + unit.cost, 0);
|
|
18643
18685
|
const restart = windowed.length > 0 ? ENGINE_START_SECONDS : 0;
|
|
18644
18686
|
const total = units.reduce((sum, unit) => sum + unit.cost, 0);
|
|
@@ -18656,6 +18698,52 @@ function dealt(units, declared, most) {
|
|
|
18656
18698
|
...bins.flatMap((bin, lane) => bin.length === 0 ? [] : [planOf(bin, declared, false, lane, units)])
|
|
18657
18699
|
];
|
|
18658
18700
|
}
|
|
18701
|
+
function slowestFirst(a, b) {
|
|
18702
|
+
return b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? "") || (a.row?.index ?? 0) - (b.row?.index ?? 0);
|
|
18703
|
+
}
|
|
18704
|
+
|
|
18705
|
+
class Shares {
|
|
18706
|
+
windowed;
|
|
18707
|
+
headless;
|
|
18708
|
+
declared;
|
|
18709
|
+
all;
|
|
18710
|
+
lanes;
|
|
18711
|
+
constructor(units, declared, lanes) {
|
|
18712
|
+
this.windowed = units.filter((unit) => unit.windowed);
|
|
18713
|
+
this.headless = units.filter((unit) => !unit.windowed).sort(slowestFirst);
|
|
18714
|
+
this.declared = declared;
|
|
18715
|
+
this.all = units;
|
|
18716
|
+
this.lanes = Math.max(1, lanes);
|
|
18717
|
+
}
|
|
18718
|
+
get empty() {
|
|
18719
|
+
return this.windowed.length === 0 && this.headless.length === 0;
|
|
18720
|
+
}
|
|
18721
|
+
next(lane) {
|
|
18722
|
+
if (this.windowed.length > 0) {
|
|
18723
|
+
const windowed = this.windowed;
|
|
18724
|
+
this.windowed = [];
|
|
18725
|
+
return planOf(windowed, this.declared, true, lane, this.all);
|
|
18726
|
+
}
|
|
18727
|
+
if (this.headless.length === 0) {
|
|
18728
|
+
return null;
|
|
18729
|
+
}
|
|
18730
|
+
const left = this.headless.reduce((sum, unit) => sum + unit.cost, 0);
|
|
18731
|
+
const target = Math.max(SHARE_FLOOR_SECONDS, left / this.lanes);
|
|
18732
|
+
const share = [];
|
|
18733
|
+
const rest = [];
|
|
18734
|
+
let cost = 0;
|
|
18735
|
+
for (const unit of this.headless) {
|
|
18736
|
+
if (share.length === 0 || cost + unit.cost <= target) {
|
|
18737
|
+
share.push(unit);
|
|
18738
|
+
cost += unit.cost;
|
|
18739
|
+
} else {
|
|
18740
|
+
rest.push(unit);
|
|
18741
|
+
}
|
|
18742
|
+
}
|
|
18743
|
+
this.headless = rest;
|
|
18744
|
+
return planOf(share, this.declared, false, lane, this.all);
|
|
18745
|
+
}
|
|
18746
|
+
}
|
|
18659
18747
|
function lanesOf(plans) {
|
|
18660
18748
|
return new Set(plans.map((plan) => plan.lane)).size;
|
|
18661
18749
|
}
|
|
@@ -18726,7 +18814,7 @@ function viewportOf(projectPath) {
|
|
|
18726
18814
|
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
18727
18815
|
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
18728
18816
|
}
|
|
18729
|
-
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5;
|
|
18817
|
+
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
|
|
18730
18818
|
var init_suite_plan = __esm(() => {
|
|
18731
18819
|
init_class_cache();
|
|
18732
18820
|
init_gdscript_source();
|
|
@@ -18735,6 +18823,7 @@ var init_suite_plan = __esm(() => {
|
|
|
18735
18823
|
NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
|
|
18736
18824
|
TEST_DIRECTORIES = ["tests", "test"];
|
|
18737
18825
|
SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
18826
|
+
SHARE_FLOOR_SECONDS = 6 * ENGINE_START_SECONDS;
|
|
18738
18827
|
});
|
|
18739
18828
|
|
|
18740
18829
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
@@ -52693,6 +52782,7 @@ ${error.text}`;
|
|
|
52693
52782
|
];
|
|
52694
52783
|
const walkStopped = leakedAtExit.length > 0 && walkStoppedPartWay(run.log.all.map((entry) => entry.text));
|
|
52695
52784
|
const countedLeaks = walkStopped ? [] : leakedAtExit;
|
|
52785
|
+
const heldNamed = countedLeaks.length > 0 ? heldAtExit(run.log.all.map((entry) => entry.text)) : undefined;
|
|
52696
52786
|
const declaredNotPrinted = judgedErrors.neverPrinted.map((missing) => ({
|
|
52697
52787
|
suite: missing.suite,
|
|
52698
52788
|
case: missing.during,
|
|
@@ -52787,7 +52877,7 @@ ${error.text}`;
|
|
|
52787
52877
|
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,
|
|
52788
52878
|
walkStopped ? WALK_STOPPED_SENTENCE : undefined,
|
|
52789
52879
|
countedLeaks.length > 0 ? [
|
|
52790
|
-
'What the engine still held as it exited fails the run, as gdharness test judges it; engineArgs ["--verbose"]
|
|
52880
|
+
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}`,
|
|
52791
52881
|
selfTypedSentence(projectSelfTypedExports(project.value.path))
|
|
52792
52882
|
].filter((part) => part !== undefined).join(" ") : undefined,
|
|
52793
52883
|
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)
|
|
@@ -58464,7 +58554,7 @@ var VALUED2 = new Set([
|
|
|
58464
58554
|
"--order",
|
|
58465
58555
|
"--result"
|
|
58466
58556
|
]);
|
|
58467
|
-
var SWITCHES = new Set(["--changed-only", "--import", "--print-output"]);
|
|
58557
|
+
var SWITCHES = new Set(["--changed-only", "--import", "--print-output", "--verbose"]);
|
|
58468
58558
|
function testOptionsOf(args, cwd) {
|
|
58469
58559
|
const valued = new Map;
|
|
58470
58560
|
const switches = new Set;
|
|
@@ -58512,6 +58602,7 @@ function testOptionsOf(args, cwd) {
|
|
|
58512
58602
|
importFirst: switches.has("--import"),
|
|
58513
58603
|
order: order === undefined ? null : order === "reversed" ? "reversed" : seedOf(order),
|
|
58514
58604
|
printOutput: switches.has("--print-output"),
|
|
58605
|
+
verbose: switches.has("--verbose"),
|
|
58515
58606
|
resultFile: last("--result") === undefined ? null : resolve10(cwd, last("--result") ?? "")
|
|
58516
58607
|
};
|
|
58517
58608
|
}
|
|
@@ -58671,7 +58762,8 @@ async function runTestCommand(options, godotPath) {
|
|
|
58671
58762
|
running,
|
|
58672
58763
|
selfTyped: selfTypedExports(scripts.sources),
|
|
58673
58764
|
godotVersion: options.godotVersion,
|
|
58674
|
-
cores: options.cores
|
|
58765
|
+
cores: options.cores,
|
|
58766
|
+
verbose: options.verbose
|
|
58675
58767
|
};
|
|
58676
58768
|
try {
|
|
58677
58769
|
if (options.importFirst) {
|
|
@@ -58743,7 +58835,19 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58743
58835
|
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(byRows)}.`);
|
|
58744
58836
|
}
|
|
58745
58837
|
const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
|
|
58746
|
-
|
|
58838
|
+
const taken = [];
|
|
58839
|
+
while (taken.length < lanes.length) {
|
|
58840
|
+
const lock = await slots.takeFree();
|
|
58841
|
+
if (lock === null) {
|
|
58842
|
+
break;
|
|
58843
|
+
}
|
|
58844
|
+
taken.push(lock);
|
|
58845
|
+
}
|
|
58846
|
+
const shared = options.order === null && lanes.length > 1 && taken.length < lanes.length;
|
|
58847
|
+
if (shared) {
|
|
58848
|
+
console.log(sharesSaid(taken.length, lanes.length));
|
|
58849
|
+
}
|
|
58850
|
+
let ran = shared ? await runShares(launch, units, declared, lanes.length, taken, times) : (await Promise.all(lanes.map((lane, at) => runLane(launch, lane, times, taken[at] ?? null)))).flat().sort((a, b) => a.index - b.index);
|
|
58747
58851
|
if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
|
|
58748
58852
|
console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
|
|
58749
58853
|
const again = [];
|
|
@@ -58761,6 +58865,9 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58761
58865
|
process14.off("SIGTERM", interrupted);
|
|
58762
58866
|
}
|
|
58763
58867
|
}
|
|
58868
|
+
function sharesSaid(free, lanes) {
|
|
58869
|
+
return `${free === 0 ? "None" : String(free)} of the ${String(lanes)} slots this run uses ${free === 1 ? "was" : "were"} free as it began, so its engines are handed the suites a share at a time as each starts, and an engine starts as each slot comes free.`;
|
|
58870
|
+
}
|
|
58764
58871
|
function roomSaid(engines, lanes, unheld, held, asked) {
|
|
58765
58872
|
if (held === undefined || lanes >= unheld) {
|
|
58766
58873
|
return "";
|
|
@@ -58839,8 +58946,8 @@ async function importProject(launch) {
|
|
|
58839
58946
|
launch.slots.release(lock);
|
|
58840
58947
|
}
|
|
58841
58948
|
}
|
|
58842
|
-
async function runLane(launch, lane, times) {
|
|
58843
|
-
const lock = await launch.slots.take();
|
|
58949
|
+
async function runLane(launch, lane, times, taken) {
|
|
58950
|
+
const lock = taken ?? await launch.slots.take();
|
|
58844
58951
|
try {
|
|
58845
58952
|
const ran = [];
|
|
58846
58953
|
for (const { plan, index } of lane) {
|
|
@@ -58851,6 +58958,39 @@ async function runLane(launch, lane, times) {
|
|
|
58851
58958
|
launch.slots.release(lock);
|
|
58852
58959
|
}
|
|
58853
58960
|
}
|
|
58961
|
+
async function runShares(launch, units, declared, lanes, taken, times) {
|
|
58962
|
+
const shares = new Shares(units, declared, lanes);
|
|
58963
|
+
const handedOut = new AbortController;
|
|
58964
|
+
let engines = 0;
|
|
58965
|
+
const lane = async (at) => {
|
|
58966
|
+
let lock = taken[at] ?? null;
|
|
58967
|
+
if (lock === null) {
|
|
58968
|
+
try {
|
|
58969
|
+
lock = await launch.slots.take(handedOut.signal);
|
|
58970
|
+
} catch (error) {
|
|
58971
|
+
if (handedOut.signal.aborted) {
|
|
58972
|
+
return [];
|
|
58973
|
+
}
|
|
58974
|
+
throw error;
|
|
58975
|
+
}
|
|
58976
|
+
}
|
|
58977
|
+
try {
|
|
58978
|
+
const ran = [];
|
|
58979
|
+
for (let plan = shares.next(at);plan !== null; plan = shares.next(at)) {
|
|
58980
|
+
if (shares.empty) {
|
|
58981
|
+
handedOut.abort();
|
|
58982
|
+
}
|
|
58983
|
+
const index = engines;
|
|
58984
|
+
engines += 1;
|
|
58985
|
+
ran.push(await runEngine2(launch, plan, index, times, "", lock));
|
|
58986
|
+
}
|
|
58987
|
+
return ran;
|
|
58988
|
+
} finally {
|
|
58989
|
+
launch.slots.release(lock);
|
|
58990
|
+
}
|
|
58991
|
+
};
|
|
58992
|
+
return (await Promise.all(Array.from({ length: lanes }, (_, at) => lane(at)))).flat().sort((a, b) => a.index - b.index);
|
|
58993
|
+
}
|
|
58854
58994
|
async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
58855
58995
|
const directory = join27(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
|
|
58856
58996
|
mkdirSync10(join27(directory, "user"), { recursive: true });
|
|
@@ -58878,6 +59018,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
|
58878
59018
|
"Dummy",
|
|
58879
59019
|
"--path",
|
|
58880
59020
|
launch.projectPath,
|
|
59021
|
+
...launch.verbose ? ["--verbose"] : [],
|
|
58881
59022
|
"-s",
|
|
58882
59023
|
shippedListedRunner(),
|
|
58883
59024
|
...plan.windowed ? [] : ["--ignoreHeadlessMode"],
|
|
@@ -59530,7 +59671,7 @@ Usage:
|
|
|
59530
59671
|
gdharness test [project] [suite ...] [--engines N] [--slots DIR] [--slot-count N]
|
|
59531
59672
|
[--silence S] [--deadline S] [--tests DIR] [--changed-only] [--reach-also PATH]
|
|
59532
59673
|
[--import] [--godot PATH] [--godot-version V] [--cores N]
|
|
59533
|
-
[--order reversed|SEED] [--print-output] [--result FILE] [--runner]
|
|
59674
|
+
[--order reversed|SEED] [--print-output] [--verbose] [--result FILE] [--runner]
|
|
59534
59675
|
Run the project's gdUnit4 suites over several engines and
|
|
59535
59676
|
judge every line they print: exit 1 on a failing case, an
|
|
59536
59677
|
engine error no case declared, a case that never finished
|
|
@@ -59539,7 +59680,8 @@ Usage:
|
|
|
59539
59680
|
suites that passed with everything they reach unchanged; it
|
|
59540
59681
|
is for iterating, and no gate passes it. --print-output
|
|
59541
59682
|
prints what every engine printed, for a
|
|
59542
|
-
benchmark run by hand. --
|
|
59683
|
+
benchmark run by hand. --verbose starts every engine verbose,
|
|
59684
|
+
so a leak at exit is named. --result writes the outcome, each
|
|
59543
59685
|
suite's time and every problem as JSON for a gate. --runner
|
|
59544
59686
|
prints the shipped runner's path. docs/test-runs.md has the
|
|
59545
59687
|
rest.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.31",
|
|
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",
|