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.
Files changed (2) hide show
  1. package/build/cli.js +158 -17
  2. 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
- faults.push(`the engine exited holding what it should have freed: ${leak}${last && selfTyped !== undefined ? ` ${selfTyped}` : ""}`);
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
- for (let slot = 0;slot < this.count; slot += 1) {
11738
- const lock = join8(this.directory, `${String(slot)}.lock`);
11739
- if (this.made(lock) || await this.takenOver(lock)) {
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((a, b) => b.cost - a.cost || a.suite.localeCompare(b.suite) || (a.cases[0] ?? "").localeCompare(b.cases[0] ?? "") || (a.row?.index ?? 0) - (b.row?.index ?? 0));
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
- let ran = (await inTurns(lanes, room.engines, (lane) => runLane(launch, lane, times))).flat().sort((a, b) => a.index - b.index);
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. --result writes the outcome, each
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.29",
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",