gdharness 1.3.20 → 1.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -10620,11 +10620,29 @@ function masked(expected, actual) {
10620
10620
  const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
10621
10621
  return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
10622
10622
  }
10623
+ function orphansLeftOn(line) {
10624
+ const named = ORPHANS_LEFT.exec(line);
10625
+ if (named === null) {
10626
+ return null;
10627
+ }
10628
+ return { suite: named[1] ?? "", during: named[2] ?? "", count: Number(named[3]), nodes: named[4] ?? "" };
10629
+ }
10630
+ function orphansLeftSaid(left) {
10631
+ const nodes = left.count === 1 ? "1 node" : `${String(left.count)} nodes`;
10632
+ const where = left.during === "after" ? `${left.suite}'s hooks` : `${left.suite} > ${left.during}`;
10633
+ return `${where} left ${nodes} neither freed nor in the tree, which gdUnit4 counts as orphans: ${left.nodes}. Free each one, or make it with auto_free(), which frees it once the case is over.`;
10634
+ }
10623
10635
  function orphansPrinted(printed) {
10624
10636
  const suites = [];
10625
10637
  let total = null;
10626
10638
  let running = null;
10639
+ const named = new Map;
10627
10640
  for (const line of printed) {
10641
+ const left = orphansLeftOn(line);
10642
+ if (left !== null) {
10643
+ named.set(left.suite, [...named.get(left.suite) ?? [], left]);
10644
+ continue;
10645
+ }
10628
10646
  const started = RUNNING_SUITE.exec(line);
10629
10647
  if (started?.[1] !== undefined) {
10630
10648
  running = started[1];
@@ -10641,13 +10659,13 @@ function orphansPrinted(printed) {
10641
10659
  continue;
10642
10660
  }
10643
10661
  if (running !== null && orphans > 0) {
10644
- suites.push({ path: running, orphans });
10662
+ suites.push({ path: running, orphans, left: [] });
10645
10663
  }
10646
10664
  running = null;
10647
10665
  }
10648
10666
  return {
10649
10667
  total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
10650
- suites
10668
+ suites: suites.map((suite) => ({ ...suite, left: named.get(suite.path) ?? [] }))
10651
10669
  };
10652
10670
  }
10653
10671
  function finishesASuite(line) {
@@ -10694,7 +10712,7 @@ function testProgressMessage(printed, elapsedMs) {
10694
10712
  const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
10695
10713
  return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
10696
10714
  }
10697
- var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
10715
+ var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, ORPHANS_LEFT, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
10698
10716
  var init_junit = __esm(() => {
10699
10717
  MalformedReportError = class MalformedReportError extends Error {
10700
10718
  constructor(message) {
@@ -10724,6 +10742,7 @@ var init_junit = __esm(() => {
10724
10742
  "<DEL>": ""
10725
10743
  };
10726
10744
  PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
10745
+ ORPHANS_LEFT = /^gdharness orphans: (res:\/\/.+?\.(?:gd|cs)) > (.+?) left (\d+): (.+?)\s*$/;
10727
10746
  RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
10728
10747
  ORPHANS = /(\d+)\s+orphans/;
10729
10748
  SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
@@ -10771,6 +10790,7 @@ function readRun(printed) {
10771
10790
  afterCase = [];
10772
10791
  };
10773
10792
  const leftOut = new Map;
10793
+ const orphansLeft = [];
10774
10794
  let engineVersion = null;
10775
10795
  for (const [at, line] of lines.entries()) {
10776
10796
  const banner = engineVersion === null ? BANNER.exec(line) : null;
@@ -10778,6 +10798,11 @@ function readRun(printed) {
10778
10798
  engineVersion = banner[1] ?? null;
10779
10799
  continue;
10780
10800
  }
10801
+ const orphaned = orphansLeftOn(line);
10802
+ if (orphaned !== null) {
10803
+ orphansLeft.push(orphaned);
10804
+ continue;
10805
+ }
10781
10806
  const left = LEFT_OUT.exec(line);
10782
10807
  if (left !== null) {
10783
10808
  const key = `${left[1] ?? ""} > ${left[2] ?? ""}`;
@@ -10864,7 +10889,19 @@ function readRun(printed) {
10864
10889
  `);
10865
10890
  const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
10866
10891
  const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
10867
- return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn, leftOut, engineVersion };
10892
+ return {
10893
+ lines,
10894
+ suites,
10895
+ errors,
10896
+ leaks,
10897
+ starved,
10898
+ executed,
10899
+ unloadable,
10900
+ stoppedIn,
10901
+ leftOut,
10902
+ engineVersion,
10903
+ orphansLeft
10904
+ };
10868
10905
  }
10869
10906
  function withNestedErrors(printed, piped) {
10870
10907
  const nested = piped.flatMap((entry, at) => {
@@ -10962,7 +10999,7 @@ function judgeEngine(printed, handed, end) {
10962
10999
  const faults = [];
10963
11000
  const failing = new Set;
10964
11001
  const shownAt = new Set;
10965
- const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED"));
11002
+ const explained = end.code === GDUNIT_FAILED && [...printed.suites.values()].some((suite) => suite.cases.some((ran) => ran.verdict === "FAILED")) || end.code === GDUNIT_ORPHANS && printed.orphansLeft.length > 0;
10966
11003
  if (end.ended !== null) {
10967
11004
  faults.push(end.ended);
10968
11005
  } else if (end.code !== 0 && !explained) {
@@ -11027,6 +11064,10 @@ function judgeEngine(printed, handed, end) {
11027
11064
  }
11028
11065
  }
11029
11066
  }
11067
+ for (const left of printed.orphansLeft) {
11068
+ faults.push(orphansLeftSaid(left));
11069
+ failing.add(left.suite);
11070
+ }
11030
11071
  const unexpected = unexpectedErrors(printed, handed.expected, handed.suites.map((suite) => suite.path));
11031
11072
  for (const error of unexpected.printed) {
11032
11073
  faults.push(error.suite === null ? `an engine ${error.warning ? "warning" : "error"} outside every suite: ${error.text}` : `an engine ${error.warning ? "warning" : "error"} in ${error.suite} > ${error.during ?? ""}: ${error.text}; ${declaringAdvice(error.during ?? "", error.warning)}`);
@@ -11138,7 +11179,7 @@ ${String(error.warning)}`);
11138
11179
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
11139
11180
  return { printed: unexpected, neverPrinted };
11140
11181
  }
11141
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11182
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11142
11183
  var init_suite_judge = __esm(() => {
11143
11184
  init_engine_leaks();
11144
11185
  init_junit();
@@ -11170,7 +11211,7 @@ var init_suite_judge = __esm(() => {
11170
11211
  MEMORY_EXITS = new Set([3221225495, 3221225773]);
11171
11212
  GDUNIT_EXITS = new Map([
11172
11213
  [GDUNIT_FAILED, "gdUnit4's code for a case that failed or errored"],
11173
- [101, "gdUnit4's code for orphan nodes left behind"],
11214
+ [GDUNIT_ORPHANS, "gdUnit4's code for orphan nodes left behind"],
11174
11215
  [103, "gdUnit4's code for a suite that cannot run headless"],
11175
11216
  [104, "gdUnit4's code for a Godot version it does not support"],
11176
11217
  [105, "gdUnit4's code for scripts that did not compile"]
@@ -11476,13 +11517,295 @@ var init_engine_launch = __esm(() => {
11476
11517
  MEMORY_PER_ENGINE = 1.5 * 1024 ** 3;
11477
11518
  });
11478
11519
 
11520
+ // src/call-signal.ts
11521
+ import { AsyncLocalStorage } from "node:async_hooks";
11522
+ function stillGoingSaid(tool, op, elapsedMs) {
11523
+ const seconds = Math.floor(elapsedMs / 1000);
11524
+ const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
11525
+ return `${op === null || op === "" ? tool : `${tool} ${op}`} is still going; ${took} in.`;
11526
+ }
11527
+ async function withCallSignal(signal, answer, progress, still) {
11528
+ if (progress === undefined) {
11529
+ return signal === undefined ? answer() : current.run({ signal, progress: undefined }, answer);
11530
+ }
11531
+ const began = Date.now();
11532
+ const rising = risingOnly(progress);
11533
+ let going = still;
11534
+ let quiet;
11535
+ const rearm = () => {
11536
+ clearTimeout(quiet);
11537
+ if (going === undefined) {
11538
+ return;
11539
+ }
11540
+ const { everyMs, said } = going;
11541
+ quiet = setTimeout(() => {
11542
+ const elapsed = Date.now() - began;
11543
+ told(Math.floor(elapsed / 1000), said(elapsed));
11544
+ }, everyMs);
11545
+ quiet.unref();
11546
+ };
11547
+ const told = (number, message) => {
11548
+ rising(number, message);
11549
+ rearm();
11550
+ };
11551
+ rearm();
11552
+ try {
11553
+ return await current.run({ signal, progress: told }, answer);
11554
+ } finally {
11555
+ going = undefined;
11556
+ clearTimeout(quiet);
11557
+ }
11558
+ }
11559
+ function risingOnly(progress) {
11560
+ let last = Number.NEGATIVE_INFINITY;
11561
+ return (number, message) => {
11562
+ last = number > last ? number : last + 1;
11563
+ progress(last, message);
11564
+ };
11565
+ }
11566
+ function callSignal() {
11567
+ return current.getStore()?.signal;
11568
+ }
11569
+ function callProgress() {
11570
+ return current.getStore()?.progress;
11571
+ }
11572
+ var current, CALL_HEARTBEAT_MS = 20000;
11573
+ var init_call_signal = __esm(() => {
11574
+ current = new AsyncLocalStorage;
11575
+ });
11576
+
11577
+ // src/engine-slots.ts
11578
+ import { closeSync as closeSync3, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync5, statSync as statSync6, writeSync as writeSync2 } from "node:fs";
11579
+ import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
11580
+ import { join as join8, resolve as resolve4 } from "node:path";
11581
+ import process7 from "node:process";
11582
+ import { setTimeout as delay3 } from "node:timers/promises";
11583
+ function machineSlots(asked = {
11584
+ directory: null,
11585
+ count: null
11586
+ }, machine = {
11587
+ env: process7.env,
11588
+ platform: process7.platform,
11589
+ processors: availableParallelism2(),
11590
+ home: homedir2(),
11591
+ cwd: process7.cwd()
11592
+ }) {
11593
+ const { env } = machine;
11594
+ const named = nonEmpty(env[SLOTS_VARIABLE]);
11595
+ const xdg = nonEmpty(env["XDG_CACHE_HOME"]);
11596
+ const local = machine.platform === "win32" ? nonEmpty(env["LOCALAPPDATA"]) : undefined;
11597
+ const cache = join8(xdg ?? local ?? join8(machine.home, ".cache"), "gdharness");
11598
+ const directory = asked.directory ?? (named === undefined ? join8(cache, "slots") : resolve4(machine.cwd, named));
11599
+ const counted = nonEmpty(env[SLOT_COUNT_VARIABLE]);
11600
+ let count = asked.count;
11601
+ if (count === null && counted !== undefined) {
11602
+ count = Number(counted);
11603
+ if (!Number.isInteger(count) || count < 1) {
11604
+ throw new EngineUsageError(`${SLOT_COUNT_VARIABLE} takes a whole number above 0, not ${counted}.`);
11605
+ }
11606
+ }
11607
+ return { directory, count: count ?? Math.max(1, Math.floor(machine.processors / PROCESSORS_A_SLOT)) };
11608
+ }
11609
+ function nonEmpty(value) {
11610
+ return value === undefined || value === "" ? undefined : value;
11611
+ }
11612
+ function holderOf(content) {
11613
+ try {
11614
+ const parsed = JSON.parse(content);
11615
+ if (typeof parsed !== "object" || parsed === null) {
11616
+ return null;
11617
+ }
11618
+ const { pid, started } = parsed;
11619
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
11620
+ } catch {
11621
+ return null;
11622
+ }
11623
+ }
11624
+ function lockIsFree(content, writtenAt, now, isAlive, startOf) {
11625
+ const holder = holderOf(content);
11626
+ if (holder === null) {
11627
+ return now - writtenAt > UNWRITTEN_MS;
11628
+ }
11629
+ if (!isAlive(holder.pid)) {
11630
+ return true;
11631
+ }
11632
+ const started = startOf(holder.pid);
11633
+ return started !== undefined && Math.abs(started - holder.started) > START_TOLERANCE_MS;
11634
+ }
11635
+ async function ownStart() {
11636
+ const said = (await startTimesOf([process7.pid])).get(process7.pid);
11637
+ return said ?? Math.round(Date.now() - process7.uptime() * 1000);
11638
+ }
11639
+
11640
+ class EngineSlots {
11641
+ starts = new Map;
11642
+ held = new Set;
11643
+ saidAt = Number.NEGATIVE_INFINITY;
11644
+ directory;
11645
+ count;
11646
+ holder;
11647
+ say;
11648
+ constructor(directory, count, holder, say) {
11649
+ this.directory = directory;
11650
+ this.count = count;
11651
+ this.holder = holder;
11652
+ this.say = say;
11653
+ }
11654
+ async take(signal, waiting) {
11655
+ mkdirSync3(this.directory, { recursive: true });
11656
+ const began = Date.now();
11657
+ let said = Number.NEGATIVE_INFINITY;
11658
+ for (;; ) {
11659
+ signal?.throwIfAborted();
11660
+ for (let slot = 0;slot < this.count; slot += 1) {
11661
+ const lock = join8(this.directory, `${String(slot)}.lock`);
11662
+ if (this.made(lock) || await this.takenOver(lock)) {
11663
+ this.held.add(lock);
11664
+ return lock;
11665
+ }
11666
+ }
11667
+ const now = Date.now();
11668
+ if (waiting !== undefined) {
11669
+ if (now - said >= WAIT_SAID_MS) {
11670
+ said = now;
11671
+ waiting(now - began);
11672
+ }
11673
+ } else if (this.say !== undefined && this.held.size === 0 && now - began >= COMMAND_WAIT_SAID_MS && now - this.saidAt >= WAIT_SAID_MS) {
11674
+ this.saidAt = now;
11675
+ this.say(slotWaitSaid(now - began, this.count, this.directory));
11676
+ }
11677
+ await delay3(SLOT_WAIT_MS, undefined, signal === undefined ? {} : { signal });
11678
+ }
11679
+ }
11680
+ releaseAll() {
11681
+ for (const lock of this.held) {
11682
+ this.release(lock);
11683
+ }
11684
+ }
11685
+ release(lock) {
11686
+ this.held.delete(lock);
11687
+ try {
11688
+ if (holderOf(readFileSync7(lock, "utf8"))?.pid === this.holder.pid) {
11689
+ rmSync5(lock, { force: true });
11690
+ }
11691
+ } catch {}
11692
+ }
11693
+ made(path) {
11694
+ let made;
11695
+ try {
11696
+ made = openSync3(path, "wx");
11697
+ } catch {
11698
+ return false;
11699
+ }
11700
+ try {
11701
+ writeSync2(made, JSON.stringify(this.holder));
11702
+ } finally {
11703
+ closeSync3(made);
11704
+ }
11705
+ return true;
11706
+ }
11707
+ async takenOver(lock) {
11708
+ if (!await this.free(lock)) {
11709
+ return false;
11710
+ }
11711
+ const steal = `${lock}.steal`;
11712
+ if (!this.made(steal)) {
11713
+ try {
11714
+ if (Date.now() - statSync6(steal).mtimeMs > UNWRITTEN_MS) {
11715
+ rmSync5(steal, { force: true });
11716
+ }
11717
+ } catch {}
11718
+ return false;
11719
+ }
11720
+ try {
11721
+ if (!await this.free(lock)) {
11722
+ return false;
11723
+ }
11724
+ try {
11725
+ rmSync5(lock, { force: true });
11726
+ } catch {
11727
+ return false;
11728
+ }
11729
+ return this.made(lock);
11730
+ } finally {
11731
+ rmSync5(steal, { force: true });
11732
+ }
11733
+ }
11734
+ async free(lock) {
11735
+ let content;
11736
+ let writtenAt;
11737
+ try {
11738
+ content = readFileSync7(lock, "utf8");
11739
+ writtenAt = statSync6(lock).mtimeMs;
11740
+ } catch {
11741
+ return false;
11742
+ }
11743
+ const pid = holderOf(content)?.pid;
11744
+ if (pid !== undefined && alive(pid)) {
11745
+ await this.readStart(pid);
11746
+ }
11747
+ return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => this.starts.get(asked)?.started);
11748
+ }
11749
+ async readStart(pid) {
11750
+ const known = this.starts.get(pid);
11751
+ if (known !== undefined && Date.now() - known.readAt < START_READ_MS) {
11752
+ return;
11753
+ }
11754
+ const started = (await startTimesOf([pid])).get(pid);
11755
+ this.starts.set(pid, { started, readAt: Date.now() });
11756
+ }
11757
+ }
11758
+ function sayWaitsTo(say) {
11759
+ saidTo = say;
11760
+ }
11761
+ function processSlots() {
11762
+ ours ??= ownStart().then((started) => {
11763
+ const chosen = machineSlots();
11764
+ return new EngineSlots(chosen.directory, chosen.count, { pid: process7.pid, started }, (line) => {
11765
+ saidTo?.(line);
11766
+ });
11767
+ });
11768
+ return ours;
11769
+ }
11770
+ function slotWaitSaid(waitedMs, count, directory) {
11771
+ const slots = count === 1 ? `the one engine slot on this machine (${directory}) is in use` : `all ${String(count)} engine slots on this machine (${directory}) are in use`;
11772
+ return `Waiting${waitedMs >= 1000 ? ` ${String(Math.round(waitedMs / 1000))} s so far` : ""} for an engine: ${slots}, by a gate or another tool, and the engine starts when one is given back.`;
11773
+ }
11774
+ async function slotForCall() {
11775
+ const slots = await processSlots();
11776
+ const progress = callProgress();
11777
+ const lock = await slots.take(callSignal(), progress === undefined ? undefined : (waitedMs) => {
11778
+ progress(waitedMs / 1000, slotWaitSaid(waitedMs, slots.count, slots.directory));
11779
+ });
11780
+ return {
11781
+ release: () => {
11782
+ slots.release(lock);
11783
+ }
11784
+ };
11785
+ }
11786
+ async function inASlot(body) {
11787
+ const held = await slotForCall();
11788
+ try {
11789
+ return await body();
11790
+ } finally {
11791
+ held.release();
11792
+ }
11793
+ }
11794
+ var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_WAIT_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, ours, saidTo;
11795
+ var init_engine_slots = __esm(() => {
11796
+ init_alive();
11797
+ init_call_signal();
11798
+ init_engine_launch();
11799
+ init_process_children();
11800
+ });
11801
+
11479
11802
  // src/detection.ts
11480
- import { existsSync as existsSync6, readdirSync as readdirSync4, statSync as statSync6 } from "node:fs";
11481
- import { homedir as homedir2 } from "node:os";
11482
- import { join as join8 } from "node:path";
11803
+ import { existsSync as existsSync6, readdirSync as readdirSync4, statSync as statSync7 } from "node:fs";
11804
+ import { homedir as homedir3 } from "node:os";
11805
+ import { join as join9 } from "node:path";
11483
11806
  function resolveHomeDirectory() {
11484
11807
  try {
11485
- return homedir2();
11808
+ return homedir3();
11486
11809
  } catch {
11487
11810
  return "";
11488
11811
  }
@@ -11503,16 +11826,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
11503
11826
  if (!pattern.test(name)) {
11504
11827
  continue;
11505
11828
  }
11506
- const fullPath = join8(directory, name);
11829
+ const fullPath = join9(directory, name);
11507
11830
  try {
11508
- const stat = statSync6(fullPath);
11831
+ const stat = statSync7(fullPath);
11509
11832
  if (stat.isFile()) {
11510
11833
  matches.push({ name, mtime: stat.mtimeMs });
11511
11834
  }
11512
11835
  } catch {}
11513
11836
  }
11514
11837
  matches.sort((a, b) => b.mtime - a.mtime);
11515
- return matches.map((m) => join8(directory, m.name));
11838
+ return matches.map((m) => join9(directory, m.name));
11516
11839
  }
11517
11840
  function conventionalPaths(platform, home) {
11518
11841
  const paths = ["godot"];
@@ -11719,7 +12042,7 @@ var init_launch = __esm(() => {
11719
12042
 
11720
12043
  // src/godot-path.ts
11721
12044
  import { existsSync as existsSync7 } from "node:fs";
11722
- import { isAbsolute as isAbsolute4, normalize, resolve as resolve4 } from "node:path";
12045
+ import { isAbsolute as isAbsolute4, normalize, resolve as resolve5 } from "node:path";
11723
12046
 
11724
12047
  class GodotLocator {
11725
12048
  answered = new Map;
@@ -11731,7 +12054,7 @@ class GodotLocator {
11731
12054
  this.found = null;
11732
12055
  const named = envValue("GODOT_PATH");
11733
12056
  if (named !== undefined) {
11734
- const path = named === "godot" || isAbsolute4(named) ? named : resolve4(named);
12057
+ const path = named === "godot" || isAbsolute4(named) ? named : resolve5(named);
11735
12058
  const reason = await this.whyNot(path);
11736
12059
  if (reason === null) {
11737
12060
  this.found = path;
@@ -11801,274 +12124,6 @@ function dictionary(entries) {
11801
12124
  return Object.assign(emptyRecord(), entries);
11802
12125
  }
11803
12126
 
11804
- // src/call-signal.ts
11805
- import { AsyncLocalStorage } from "node:async_hooks";
11806
- function stillGoingSaid(tool, op, elapsedMs) {
11807
- const seconds = Math.floor(elapsedMs / 1000);
11808
- const took = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
11809
- return `${op === null || op === "" ? tool : `${tool} ${op}`} is still going; ${took} in.`;
11810
- }
11811
- async function withCallSignal(signal, answer, progress, still) {
11812
- if (progress === undefined) {
11813
- return signal === undefined ? answer() : current.run({ signal, progress: undefined }, answer);
11814
- }
11815
- const began = Date.now();
11816
- const rising = risingOnly(progress);
11817
- let going = still;
11818
- let quiet;
11819
- const rearm = () => {
11820
- clearTimeout(quiet);
11821
- if (going === undefined) {
11822
- return;
11823
- }
11824
- const { everyMs, said } = going;
11825
- quiet = setTimeout(() => {
11826
- const elapsed = Date.now() - began;
11827
- told(Math.floor(elapsed / 1000), said(elapsed));
11828
- }, everyMs);
11829
- quiet.unref();
11830
- };
11831
- const told = (number, message) => {
11832
- rising(number, message);
11833
- rearm();
11834
- };
11835
- rearm();
11836
- try {
11837
- return await current.run({ signal, progress: told }, answer);
11838
- } finally {
11839
- going = undefined;
11840
- clearTimeout(quiet);
11841
- }
11842
- }
11843
- function risingOnly(progress) {
11844
- let last = Number.NEGATIVE_INFINITY;
11845
- return (number, message) => {
11846
- last = number > last ? number : last + 1;
11847
- progress(last, message);
11848
- };
11849
- }
11850
- function callSignal() {
11851
- return current.getStore()?.signal;
11852
- }
11853
- function callProgress() {
11854
- return current.getStore()?.progress;
11855
- }
11856
- var current, CALL_HEARTBEAT_MS = 20000;
11857
- var init_call_signal = __esm(() => {
11858
- current = new AsyncLocalStorage;
11859
- });
11860
-
11861
- // src/engine-slots.ts
11862
- import { closeSync as closeSync3, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync5, statSync as statSync7, writeSync as writeSync2 } from "node:fs";
11863
- import { availableParallelism as availableParallelism2, homedir as homedir3 } from "node:os";
11864
- import { join as join9, resolve as resolve5 } from "node:path";
11865
- import process8 from "node:process";
11866
- import { setTimeout as delay3 } from "node:timers/promises";
11867
- function machineSlots(asked = {
11868
- directory: null,
11869
- count: null
11870
- }, machine = {
11871
- env: process8.env,
11872
- platform: process8.platform,
11873
- processors: availableParallelism2(),
11874
- home: homedir3(),
11875
- cwd: process8.cwd()
11876
- }) {
11877
- const { env } = machine;
11878
- const named = nonEmpty(env[SLOTS_VARIABLE]);
11879
- const xdg = nonEmpty(env["XDG_CACHE_HOME"]);
11880
- const local = machine.platform === "win32" ? nonEmpty(env["LOCALAPPDATA"]) : undefined;
11881
- const cache = join9(xdg ?? local ?? join9(machine.home, ".cache"), "gdharness");
11882
- const directory = asked.directory ?? (named === undefined ? join9(cache, "slots") : resolve5(machine.cwd, named));
11883
- const counted = nonEmpty(env[SLOT_COUNT_VARIABLE]);
11884
- let count = asked.count;
11885
- if (count === null && counted !== undefined) {
11886
- count = Number(counted);
11887
- if (!Number.isInteger(count) || count < 1) {
11888
- throw new EngineUsageError(`${SLOT_COUNT_VARIABLE} takes a whole number above 0, not ${counted}.`);
11889
- }
11890
- }
11891
- return { directory, count: count ?? Math.max(1, Math.floor(machine.processors / PROCESSORS_A_SLOT)) };
11892
- }
11893
- function nonEmpty(value) {
11894
- return value === undefined || value === "" ? undefined : value;
11895
- }
11896
- function holderOf(content) {
11897
- try {
11898
- const parsed = JSON.parse(content);
11899
- if (typeof parsed !== "object" || parsed === null) {
11900
- return null;
11901
- }
11902
- const { pid, started } = parsed;
11903
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
11904
- } catch {
11905
- return null;
11906
- }
11907
- }
11908
- function lockIsFree(content, writtenAt, now, isAlive, startOf) {
11909
- const holder = holderOf(content);
11910
- if (holder === null) {
11911
- return now - writtenAt > UNWRITTEN_MS;
11912
- }
11913
- if (!isAlive(holder.pid)) {
11914
- return true;
11915
- }
11916
- const started = startOf(holder.pid);
11917
- return started !== undefined && Math.abs(started - holder.started) > START_TOLERANCE_MS;
11918
- }
11919
- async function ownStart() {
11920
- const said = (await startTimesOf([process8.pid])).get(process8.pid);
11921
- return said ?? Math.round(Date.now() - process8.uptime() * 1000);
11922
- }
11923
-
11924
- class EngineSlots {
11925
- starts = new Map;
11926
- held = new Set;
11927
- directory;
11928
- count;
11929
- holder;
11930
- constructor(directory, count, holder) {
11931
- this.directory = directory;
11932
- this.count = count;
11933
- this.holder = holder;
11934
- }
11935
- async take(signal, waiting) {
11936
- mkdirSync3(this.directory, { recursive: true });
11937
- const began = Date.now();
11938
- let said = Number.NEGATIVE_INFINITY;
11939
- for (;; ) {
11940
- signal?.throwIfAborted();
11941
- for (let slot = 0;slot < this.count; slot += 1) {
11942
- const lock = join9(this.directory, `${String(slot)}.lock`);
11943
- if (this.made(lock) || await this.takenOver(lock)) {
11944
- this.held.add(lock);
11945
- return lock;
11946
- }
11947
- }
11948
- if (waiting !== undefined && Date.now() - said >= WAIT_SAID_MS) {
11949
- said = Date.now();
11950
- waiting(said - began);
11951
- }
11952
- await delay3(SLOT_WAIT_MS, undefined, signal === undefined ? {} : { signal });
11953
- }
11954
- }
11955
- releaseAll() {
11956
- for (const lock of this.held) {
11957
- this.release(lock);
11958
- }
11959
- }
11960
- release(lock) {
11961
- this.held.delete(lock);
11962
- try {
11963
- if (holderOf(readFileSync7(lock, "utf8"))?.pid === this.holder.pid) {
11964
- rmSync5(lock, { force: true });
11965
- }
11966
- } catch {}
11967
- }
11968
- made(path) {
11969
- let made;
11970
- try {
11971
- made = openSync3(path, "wx");
11972
- } catch {
11973
- return false;
11974
- }
11975
- try {
11976
- writeSync2(made, JSON.stringify(this.holder));
11977
- } finally {
11978
- closeSync3(made);
11979
- }
11980
- return true;
11981
- }
11982
- async takenOver(lock) {
11983
- if (!await this.free(lock)) {
11984
- return false;
11985
- }
11986
- const steal = `${lock}.steal`;
11987
- if (!this.made(steal)) {
11988
- try {
11989
- if (Date.now() - statSync7(steal).mtimeMs > UNWRITTEN_MS) {
11990
- rmSync5(steal, { force: true });
11991
- }
11992
- } catch {}
11993
- return false;
11994
- }
11995
- try {
11996
- if (!await this.free(lock)) {
11997
- return false;
11998
- }
11999
- try {
12000
- rmSync5(lock, { force: true });
12001
- } catch {
12002
- return false;
12003
- }
12004
- return this.made(lock);
12005
- } finally {
12006
- rmSync5(steal, { force: true });
12007
- }
12008
- }
12009
- async free(lock) {
12010
- let content;
12011
- let writtenAt;
12012
- try {
12013
- content = readFileSync7(lock, "utf8");
12014
- writtenAt = statSync7(lock).mtimeMs;
12015
- } catch {
12016
- return false;
12017
- }
12018
- const pid = holderOf(content)?.pid;
12019
- if (pid !== undefined && alive(pid)) {
12020
- await this.readStart(pid);
12021
- }
12022
- return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => this.starts.get(asked)?.started);
12023
- }
12024
- async readStart(pid) {
12025
- const known = this.starts.get(pid);
12026
- if (known !== undefined && Date.now() - known.readAt < START_READ_MS) {
12027
- return;
12028
- }
12029
- const started = (await startTimesOf([pid])).get(pid);
12030
- this.starts.set(pid, { started, readAt: Date.now() });
12031
- }
12032
- }
12033
- function processSlots() {
12034
- ours ??= ownStart().then((started) => {
12035
- const chosen = machineSlots();
12036
- return new EngineSlots(chosen.directory, chosen.count, { pid: process8.pid, started });
12037
- });
12038
- return ours;
12039
- }
12040
- function slotWaitSaid(waitedMs, count, directory) {
12041
- const slots = count === 1 ? `the one engine slot on this machine (${directory}) is in use` : `all ${String(count)} engine slots on this machine (${directory}) are in use`;
12042
- return `Waiting${waitedMs >= 1000 ? ` ${String(Math.round(waitedMs / 1000))} s so far` : ""} for an engine: ${slots}, by a gate or another tool, and the engine starts when one is given back.`;
12043
- }
12044
- async function slotForCall() {
12045
- const slots = await processSlots();
12046
- const progress = callProgress();
12047
- const lock = await slots.take(callSignal(), progress === undefined ? undefined : (waitedMs) => {
12048
- progress(waitedMs / 1000, slotWaitSaid(waitedMs, slots.count, slots.directory));
12049
- });
12050
- return {
12051
- release: () => {
12052
- slots.release(lock);
12053
- }
12054
- };
12055
- }
12056
- async function inASlot(body) {
12057
- const held = await slotForCall();
12058
- try {
12059
- return await body();
12060
- } finally {
12061
- held.release();
12062
- }
12063
- }
12064
- var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_WAIT_MS = 50, WAIT_SAID_MS = 20000, START_READ_MS = 1e4, ours;
12065
- var init_engine_slots = __esm(() => {
12066
- init_alive();
12067
- init_call_signal();
12068
- init_engine_launch();
12069
- init_process_children();
12070
- });
12071
-
12072
12127
  // src/game-log.ts
12073
12128
  import { StringDecoder } from "node:string_decoder";
12074
12129
  function announced(line) {
@@ -16407,7 +16462,7 @@ var init_tool_definitions = __esm(() => {
16407
16462
  title: "Run tests",
16408
16463
  effect: "additive",
16409
16464
  description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
16410
- answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources or memory pages, 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. 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,
16465
+ answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw, and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources or memory pages, 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 (after for the suite's hooks) 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,
16411
16466
  parameters: {
16412
16467
  projectPath: PROJECT_PATH,
16413
16468
  path: {
@@ -16680,7 +16735,7 @@ var init_tool_definitions = __esm(() => {
16680
16735
  {
16681
16736
  name: "scene_animation",
16682
16737
  title: "Animations",
16683
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused. A key add_track writes past the animation's end, where it never plays, is written and named under pastLength. remove_track takes out every track of the type, nodePath and property given, with its keys; a method track given a method loses only the keys calling it, and goes once none is left. read_states gives each state that plays an animation animationFound, and animationMissing saying why when the player lacks it. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it, which stay in their trees playing nothing until remove_state takes them out. Every state machine has Start and End, which cannot be added or removed; a transition from Start sets the state it begins in. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around. " + ALSO_SAVED,
16738
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. The player and the tree are nodes, so scene_node add puts one in the scene first, and the animation a player plays on load is its autoplay property, which scene_node set sets. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. add_track on a path the animation already has a track of that type for adds the keys to that track, as the editor keys a property it already animates, answering existingTrack true and keysAdded beside keys, the track's count; a key at a time that track already has a key at is refused unless replaceKeys is true, which is how one key's value is changed. A key add_track writes past the animation's end, where it never plays, is written and named under pastLength. remove_track takes out every track of the type, nodePath and property given, with its keys; a method track given a method loses only the keys calling it, and goes once none is left. read_states gives each state that plays an animation animationFound, and animationMissing saying why when the player lacks it. remove clears the player's autoplay when it named the animation and names under statesPlayingNothing the states that played it, which stay in their trees playing nothing until remove_state takes them out. Every state machine has Start and End, which cannot be added or removed; a transition from Start sets the state it begins in. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around. " + ALSO_SAVED,
16684
16739
  parameters: {
16685
16740
  projectPath: PROJECT_PATH,
16686
16741
  scenePath: SCENE_PATH,
@@ -16716,6 +16771,11 @@ var init_tool_definitions = __esm(() => {
16716
16771
  description: "create and set: keyframe snap in seconds, 0 or more. create: default 0.1."
16717
16772
  },
16718
16773
  track: { ...ANIMATION_TRACK, ops: ["add_track", "remove_track"] },
16774
+ replaceKeys: {
16775
+ type: "boolean",
16776
+ ops: ["add_track"],
16777
+ description: "add_track: true puts a keyframe at a time the track already has a key at in that key's place, and names each key replaced under keysReplaced, its time and the value it held (a method key's method and args); keysAdded then counts only the new times. Default false, which refuses the call and writes nothing."
16778
+ },
16719
16779
  animTreePath: {
16720
16780
  type: "string",
16721
16781
  ops: ["read_states", "add_state", "remove_state", "connect_states", "disconnect_states"],
@@ -16944,7 +17004,7 @@ var init_tool_definitions = __esm(() => {
16944
17004
  {
16945
17005
  name: "resource_edit",
16946
17006
  title: "Edit resources",
16947
- description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
17007
+ description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color". A Curve, Curve2D or Curve3D reads as the inspector shows it, point_count and point_0/position and the rest, rather than the _data list the file stores, and so does one built into a resource or a scene. create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
16948
17008
  parameters: {
16949
17009
  projectPath: PROJECT_PATH,
16950
17010
  resourcePath: {
@@ -52462,7 +52522,8 @@ ${platform}`;
52462
52522
  path: suite.path,
52463
52523
  name: report.suites.find((entry) => entry.path === suite.path)?.name ?? suite.path,
52464
52524
  orphans: suite.orphans,
52465
- message: `${suite.orphans === 1 ? "1 node" : `${String(suite.orphans)} nodes`} built during this suite ${suite.orphans === 1 ? "was" : "were"} neither freed nor in the tree when it finished, which gdUnit4 counts as orphans. Free each one, or make it with auto_free(), which frees it once the case is over.`
52525
+ ...suite.left.length > 0 ? { left: suite.left.map(({ during, count, nodes }) => ({ case: during, count, nodes })) } : {},
52526
+ message: suite.left.length > 0 ? suite.left.map(orphansLeftSaid).join(" ") : `${suite.orphans === 1 ? "1 node" : `${String(suite.orphans)} nodes`} built during this suite ${suite.orphans === 1 ? "was" : "were"} neither freed nor in the tree when it finished, which gdUnit4 counts as orphans. Free each one, or make it with auto_free(), which frees it once the case is over.`
52466
52527
  }));
52467
52528
  const judgedErrors = unexpectedErrors(casePrinted, declaredErrors, [...casePrinted.suites.keys()]);
52468
52529
  const uncaughtTexts = new Set(uncaught.map((entry) => entry.text));
@@ -57184,15 +57245,16 @@ var init_server = __esm(() => {
57184
57245
  // src/cli.ts
57185
57246
  init_bridge_announce();
57186
57247
  init_engine_launch();
57248
+ init_engine_slots();
57187
57249
  import { existsSync as existsSync35 } from "node:fs";
57188
57250
  import { dirname as dirname26, join as join44, resolve as resolve14 } from "node:path";
57189
57251
 
57190
57252
  // src/error-mode.ts
57191
- import process7 from "node:process";
57253
+ import process8 from "node:process";
57192
57254
  var NO_FAULT_ERROR_BOX = 2;
57193
- var cleared = process7.platform !== "win32";
57255
+ var cleared = process8.platform !== "win32";
57194
57256
  async function letWindowsReportCrashes() {
57195
- if (cleared || !("bun" in process7.versions)) {
57257
+ if (cleared || !("bun" in process8.versions)) {
57196
57258
  return;
57197
57259
  }
57198
57260
  try {
@@ -57456,7 +57518,7 @@ metadata:
57456
57518
 
57457
57519
  # gdharness
57458
57520
 
57459
- An MCP server and three Godot addons. Together they answer about the editor that is open, the game
57521
+ An MCP server and four Godot addons. Together they answer about the editor that is open, the game
57460
57522
  that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`domain_verb\`.
57461
57523
 
57462
57524
  \`references/tools.md\` is every tool, op and argument.
@@ -57885,7 +57947,9 @@ async function runSpreadCommand(options, godotPath) {
57885
57947
  mkdirSync8(shareDirectory, { recursive: true });
57886
57948
  const chosen = machineSlots({ directory: options.slotsDirectory, count: options.slotCount });
57887
57949
  const slotCount = chosen.count;
57888
- const slots = new EngineSlots(chosen.directory, slotCount, { pid: process13.pid, started: await ownStart() });
57950
+ const slots = new EngineSlots(chosen.directory, slotCount, { pid: process13.pid, started: await ownStart() }, (line) => {
57951
+ console.log(`[spread] ${line}`);
57952
+ });
57889
57953
  const launch = {
57890
57954
  godotPath,
57891
57955
  projectPath: options.projectPath,
@@ -58416,9 +58480,8 @@ async function runTestCommand(options, godotPath) {
58416
58480
  mkdirSync10(runDirectory, { recursive: true });
58417
58481
  const logs = new RunLogs(runDirectory, keptRuns === testRuns ? null : join27(keptRuns, runName));
58418
58482
  const chosen = machineSlots({ directory: options.slotsDirectory, count: options.slotCount });
58419
- const slots = new EngineSlots(chosen.directory, chosen.count, {
58420
- pid: process14.pid,
58421
- started: await ownStart()
58483
+ const slots = new EngineSlots(chosen.directory, chosen.count, { pid: process14.pid, started: await ownStart() }, (line) => {
58484
+ console.log(line);
58422
58485
  });
58423
58486
  const running = new Set;
58424
58487
  const interrupted = () => {
@@ -59126,6 +59189,9 @@ async function upgrade() {
59126
59189
  const digestBefore = installedEditorDigest(projectPath);
59127
59190
  for (const addon of installAddons(projectPath)) {
59128
59191
  console.log(`${addon.replaced ? "replaced" : "installed"} ${addon.path}`);
59192
+ if (!addon.replaced) {
59193
+ console.log(` addons/${addon.name}/ is new to this project: a .gitignore entry or export filter naming gdharness's addons one by one has to name it too`);
59194
+ }
59129
59195
  }
59130
59196
  const enabled = await enablePlugins(godot, projectPath, EDITOR_PLUGINS);
59131
59197
  for (const [index, outcome] of enabled.entries()) {
@@ -59359,6 +59425,11 @@ async function spread() {
59359
59425
  }
59360
59426
  async function main() {
59361
59427
  await letWindowsReportCrashes();
59428
+ if (command !== undefined) {
59429
+ sayWaitsTo((line) => {
59430
+ console.error(`gdharness: ${line}`);
59431
+ });
59432
+ }
59362
59433
  if (command === "test") {
59363
59434
  await test();
59364
59435
  return;