gdharness 1.3.29 → 1.3.30
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 +158 -17
- 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;
|
|
@@ -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
|
|
@@ -58464,7 +58553,7 @@ var VALUED2 = new Set([
|
|
|
58464
58553
|
"--order",
|
|
58465
58554
|
"--result"
|
|
58466
58555
|
]);
|
|
58467
|
-
var SWITCHES = new Set(["--changed-only", "--import", "--print-output"]);
|
|
58556
|
+
var SWITCHES = new Set(["--changed-only", "--import", "--print-output", "--verbose"]);
|
|
58468
58557
|
function testOptionsOf(args, cwd) {
|
|
58469
58558
|
const valued = new Map;
|
|
58470
58559
|
const switches = new Set;
|
|
@@ -58512,6 +58601,7 @@ function testOptionsOf(args, cwd) {
|
|
|
58512
58601
|
importFirst: switches.has("--import"),
|
|
58513
58602
|
order: order === undefined ? null : order === "reversed" ? "reversed" : seedOf(order),
|
|
58514
58603
|
printOutput: switches.has("--print-output"),
|
|
58604
|
+
verbose: switches.has("--verbose"),
|
|
58515
58605
|
resultFile: last("--result") === undefined ? null : resolve10(cwd, last("--result") ?? "")
|
|
58516
58606
|
};
|
|
58517
58607
|
}
|
|
@@ -58671,7 +58761,8 @@ async function runTestCommand(options, godotPath) {
|
|
|
58671
58761
|
running,
|
|
58672
58762
|
selfTyped: selfTypedExports(scripts.sources),
|
|
58673
58763
|
godotVersion: options.godotVersion,
|
|
58674
|
-
cores: options.cores
|
|
58764
|
+
cores: options.cores,
|
|
58765
|
+
verbose: options.verbose
|
|
58675
58766
|
};
|
|
58676
58767
|
try {
|
|
58677
58768
|
if (options.importFirst) {
|
|
@@ -58743,7 +58834,19 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58743
58834
|
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
58835
|
}
|
|
58745
58836
|
const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
|
|
58746
|
-
|
|
58837
|
+
const taken = [];
|
|
58838
|
+
while (taken.length < lanes.length) {
|
|
58839
|
+
const lock = await slots.takeFree();
|
|
58840
|
+
if (lock === null) {
|
|
58841
|
+
break;
|
|
58842
|
+
}
|
|
58843
|
+
taken.push(lock);
|
|
58844
|
+
}
|
|
58845
|
+
const shared = options.order === null && lanes.length > 1 && taken.length < lanes.length;
|
|
58846
|
+
if (shared) {
|
|
58847
|
+
console.log(sharesSaid(taken.length, lanes.length));
|
|
58848
|
+
}
|
|
58849
|
+
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
58850
|
if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
|
|
58748
58851
|
console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
|
|
58749
58852
|
const again = [];
|
|
@@ -58761,6 +58864,9 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58761
58864
|
process14.off("SIGTERM", interrupted);
|
|
58762
58865
|
}
|
|
58763
58866
|
}
|
|
58867
|
+
function sharesSaid(free, lanes) {
|
|
58868
|
+
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.`;
|
|
58869
|
+
}
|
|
58764
58870
|
function roomSaid(engines, lanes, unheld, held, asked) {
|
|
58765
58871
|
if (held === undefined || lanes >= unheld) {
|
|
58766
58872
|
return "";
|
|
@@ -58839,8 +58945,8 @@ async function importProject(launch) {
|
|
|
58839
58945
|
launch.slots.release(lock);
|
|
58840
58946
|
}
|
|
58841
58947
|
}
|
|
58842
|
-
async function runLane(launch, lane, times) {
|
|
58843
|
-
const lock = await launch.slots.take();
|
|
58948
|
+
async function runLane(launch, lane, times, taken) {
|
|
58949
|
+
const lock = taken ?? await launch.slots.take();
|
|
58844
58950
|
try {
|
|
58845
58951
|
const ran = [];
|
|
58846
58952
|
for (const { plan, index } of lane) {
|
|
@@ -58851,6 +58957,39 @@ async function runLane(launch, lane, times) {
|
|
|
58851
58957
|
launch.slots.release(lock);
|
|
58852
58958
|
}
|
|
58853
58959
|
}
|
|
58960
|
+
async function runShares(launch, units, declared, lanes, taken, times) {
|
|
58961
|
+
const shares = new Shares(units, declared, lanes);
|
|
58962
|
+
const handedOut = new AbortController;
|
|
58963
|
+
let engines = 0;
|
|
58964
|
+
const lane = async (at) => {
|
|
58965
|
+
let lock = taken[at] ?? null;
|
|
58966
|
+
if (lock === null) {
|
|
58967
|
+
try {
|
|
58968
|
+
lock = await launch.slots.take(handedOut.signal);
|
|
58969
|
+
} catch (error) {
|
|
58970
|
+
if (handedOut.signal.aborted) {
|
|
58971
|
+
return [];
|
|
58972
|
+
}
|
|
58973
|
+
throw error;
|
|
58974
|
+
}
|
|
58975
|
+
}
|
|
58976
|
+
try {
|
|
58977
|
+
const ran = [];
|
|
58978
|
+
for (let plan = shares.next(at);plan !== null; plan = shares.next(at)) {
|
|
58979
|
+
if (shares.empty) {
|
|
58980
|
+
handedOut.abort();
|
|
58981
|
+
}
|
|
58982
|
+
const index = engines;
|
|
58983
|
+
engines += 1;
|
|
58984
|
+
ran.push(await runEngine2(launch, plan, index, times, "", lock));
|
|
58985
|
+
}
|
|
58986
|
+
return ran;
|
|
58987
|
+
} finally {
|
|
58988
|
+
launch.slots.release(lock);
|
|
58989
|
+
}
|
|
58990
|
+
};
|
|
58991
|
+
return (await Promise.all(Array.from({ length: lanes }, (_, at) => lane(at)))).flat().sort((a, b) => a.index - b.index);
|
|
58992
|
+
}
|
|
58854
58993
|
async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
58855
58994
|
const directory = join27(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
|
|
58856
58995
|
mkdirSync10(join27(directory, "user"), { recursive: true });
|
|
@@ -58878,6 +59017,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
|
58878
59017
|
"Dummy",
|
|
58879
59018
|
"--path",
|
|
58880
59019
|
launch.projectPath,
|
|
59020
|
+
...launch.verbose ? ["--verbose"] : [],
|
|
58881
59021
|
"-s",
|
|
58882
59022
|
shippedListedRunner(),
|
|
58883
59023
|
...plan.windowed ? [] : ["--ignoreHeadlessMode"],
|
|
@@ -59530,7 +59670,7 @@ Usage:
|
|
|
59530
59670
|
gdharness test [project] [suite ...] [--engines N] [--slots DIR] [--slot-count N]
|
|
59531
59671
|
[--silence S] [--deadline S] [--tests DIR] [--changed-only] [--reach-also PATH]
|
|
59532
59672
|
[--import] [--godot PATH] [--godot-version V] [--cores N]
|
|
59533
|
-
[--order reversed|SEED] [--print-output] [--result FILE] [--runner]
|
|
59673
|
+
[--order reversed|SEED] [--print-output] [--verbose] [--result FILE] [--runner]
|
|
59534
59674
|
Run the project's gdUnit4 suites over several engines and
|
|
59535
59675
|
judge every line they print: exit 1 on a failing case, an
|
|
59536
59676
|
engine error no case declared, a case that never finished
|
|
@@ -59539,7 +59679,8 @@ Usage:
|
|
|
59539
59679
|
suites that passed with everything they reach unchanged; it
|
|
59540
59680
|
is for iterating, and no gate passes it. --print-output
|
|
59541
59681
|
prints what every engine printed, for a
|
|
59542
|
-
benchmark run by hand. --
|
|
59682
|
+
benchmark run by hand. --verbose starts every engine verbose,
|
|
59683
|
+
so a leak at exit is named. --result writes the outcome, each
|
|
59543
59684
|
suite's time and every problem as JSON for a gate. --runner
|
|
59544
59685
|
prints the shipped runner's path. docs/test-runs.md has the
|
|
59545
59686
|
rest.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.30",
|
|
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",
|