gdharness 1.3.11 → 1.3.12

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
@@ -10094,7 +10094,15 @@ var init_import_freshness = __esm(() => {
10094
10094
  });
10095
10095
 
10096
10096
  // src/library-copies.ts
10097
- import { existsSync as existsSync9, readdirSync as readdirSync7, readFileSync as readFileSync7, renameSync, unlinkSync } from "node:fs";
10097
+ import {
10098
+ existsSync as existsSync9,
10099
+ mkdirSync as mkdirSync3,
10100
+ readdirSync as readdirSync7,
10101
+ readFileSync as readFileSync7,
10102
+ renameSync,
10103
+ unlinkSync,
10104
+ writeFileSync as writeFileSync3
10105
+ } from "node:fs";
10098
10106
  import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute5, join as join10 } from "node:path";
10099
10107
  function freeLibraryCopies(projectPath, platform = process.platform) {
10100
10108
  const setAside = [];
@@ -10129,6 +10137,44 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
10129
10137
  }
10130
10138
  return { setAside, swept };
10131
10139
  }
10140
+ function seedExtensionList(projectPath) {
10141
+ const list = join10(projectPath, ".godot", "extension_list.cfg");
10142
+ if (existsSync9(list)) {
10143
+ return null;
10144
+ }
10145
+ const found = [];
10146
+ const visit = (directory, resDirectory) => {
10147
+ if (existsSync9(join10(directory, ".gdignore"))) {
10148
+ return;
10149
+ }
10150
+ let entries;
10151
+ try {
10152
+ entries = readdirSync7(directory, { withFileTypes: true });
10153
+ } catch {
10154
+ return;
10155
+ }
10156
+ for (const entry of entries) {
10157
+ if (entry.name.startsWith(".")) {
10158
+ continue;
10159
+ }
10160
+ if (entry.isDirectory()) {
10161
+ visit(join10(directory, entry.name), `${resDirectory}${entry.name}/`);
10162
+ } else if (entry.name.endsWith(".gdextension")) {
10163
+ found.push(`${resDirectory}${entry.name}`);
10164
+ }
10165
+ }
10166
+ };
10167
+ visit(projectPath, "res://");
10168
+ if (found.length === 0) {
10169
+ return [];
10170
+ }
10171
+ found.sort();
10172
+ mkdirSync3(dirname6(list), { recursive: true });
10173
+ writeFileSync3(list, `${found.join(`
10174
+ `)}
10175
+ `);
10176
+ return found;
10177
+ }
10132
10178
  function windowsLibraries(projectPath) {
10133
10179
  const listed = readOrNull(join10(projectPath, ".godot", "extension_list.cfg"));
10134
10180
  if (listed === null) {
@@ -10177,7 +10223,7 @@ var init_library_copies = () => {};
10177
10223
 
10178
10224
  // src/reimport.ts
10179
10225
  import { createHash as createHash2 } from "node:crypto";
10180
- import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync3, statSync as statSync8, utimesSync, writeFileSync as writeFileSync3 } from "node:fs";
10226
+ import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync3, statSync as statSync8, utimesSync, writeFileSync as writeFileSync4 } from "node:fs";
10181
10227
  import { basename as basename7, join as join11 } from "node:path";
10182
10228
  function forceNextImport(projectPath, resourcePath) {
10183
10229
  const relative = resourcePath.replace(/^res:\/\//, "");
@@ -10186,7 +10232,7 @@ function forceNextImport(projectPath, resourcePath) {
10186
10232
  return;
10187
10233
  }
10188
10234
  const { atime, mtimeMs } = statSync8(sidecar);
10189
- writeFileSync3(sidecar, readFileSync8(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
10235
+ writeFileSync4(sidecar, readFileSync8(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
10190
10236
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
10191
10237
  rmSync3(importRecord(projectPath, resourcePath), { force: true });
10192
10238
  }
@@ -10961,7 +11007,7 @@ var init_script_origins = __esm(() => {
10961
11007
  });
10962
11008
 
10963
11009
  // src/headless.ts
10964
- import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
11010
+ import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
10965
11011
  import { dirname as dirname7, join as join14 } from "node:path";
10966
11012
  function snakeCased(params) {
10967
11013
  const result = emptyRecord();
@@ -11156,7 +11202,7 @@ async function operationOutcome(engine, operation, params, projectPath) {
11156
11202
  const paramsDir = scratchDirectory("params");
11157
11203
  const paramsFile = join14(paramsDir, `${operation}.json`);
11158
11204
  const answerFile = join14(paramsDir, "answer.json");
11159
- writeFileSync4(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
11205
+ writeFileSync5(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
11160
11206
  const args = [
11161
11207
  "--headless",
11162
11208
  ...engine.editor === true ? ["--editor"] : [],
@@ -11277,7 +11323,7 @@ var init_headless = __esm(() => {
11277
11323
  });
11278
11324
 
11279
11325
  // src/update-check.ts
11280
- import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
11326
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
11281
11327
  import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
11282
11328
  import { join as join15 } from "node:path";
11283
11329
  function packageUrl(root) {
@@ -11311,7 +11357,7 @@ function cacheDirectory(environment) {
11311
11357
  function cacheFile(environment = process.env) {
11312
11358
  try {
11313
11359
  const directory = cacheDirectory(environment);
11314
- mkdirSync3(directory, { recursive: true, mode: 448 });
11360
+ mkdirSync4(directory, { recursive: true, mode: 448 });
11315
11361
  return join15(directory, "update-check.json");
11316
11362
  } catch {
11317
11363
  return join15(tmpdir3(), "gdharness-update-check.json");
@@ -11339,7 +11385,7 @@ function readCache(path) {
11339
11385
  }
11340
11386
  function writeCache(path, entry) {
11341
11387
  try {
11342
- writeFileSync5(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
11388
+ writeFileSync6(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
11343
11389
  } catch {}
11344
11390
  }
11345
11391
  function parts(version) {
@@ -12023,13 +12069,13 @@ import { createHash as createHash4 } from "node:crypto";
12023
12069
  import {
12024
12070
  cpSync,
12025
12071
  existsSync as existsSync14,
12026
- mkdirSync as mkdirSync4,
12072
+ mkdirSync as mkdirSync5,
12027
12073
  readdirSync as readdirSync9,
12028
12074
  readFileSync as readFileSync13,
12029
12075
  rmdirSync,
12030
12076
  rmSync as rmSync5,
12031
12077
  statSync as statSync9,
12032
- writeFileSync as writeFileSync6
12078
+ writeFileSync as writeFileSync7
12033
12079
  } from "node:fs";
12034
12080
  import { dirname as dirname8, join as join16 } from "node:path";
12035
12081
  import { fileURLToPath } from "node:url";
@@ -12118,15 +12164,15 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
12118
12164
  const target = join16(projectPath, "addons", name);
12119
12165
  const replaced = existsSync14(target);
12120
12166
  rmSync5(target, { recursive: true, force: true });
12121
- mkdirSync4(dirname8(target), { recursive: true });
12167
+ mkdirSync5(dirname8(target), { recursive: true });
12122
12168
  cpSync(source, target, { recursive: true });
12123
- writeFileSync6(join16(target, VERSION_MARKER), `${SERVER_VERSION}
12169
+ writeFileSync7(join16(target, VERSION_MARKER), `${SERVER_VERSION}
12124
12170
  `);
12125
12171
  installed.push({ name, path: target, replaced });
12126
12172
  }
12127
- writeFileSync6(join16(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
12173
+ writeFileSync7(join16(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
12128
12174
  `);
12129
- writeFileSync6(join16(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
12175
+ writeFileSync7(join16(projectPath, "addons", RUNTIME_ADDON, DIGEST_MARKER), `${runtimeAddonDigest(from)}
12130
12176
  `);
12131
12177
  return installed;
12132
12178
  }
@@ -14767,7 +14813,7 @@ import {
14767
14813
  readFileSync as readFileSync14,
14768
14814
  rmSync as rmSync7,
14769
14815
  statSync as statSync10,
14770
- writeFileSync as writeFileSync8,
14816
+ writeFileSync as writeFileSync9,
14771
14817
  writeSync
14772
14818
  } from "node:fs";
14773
14819
  import { tmpdir as tmpdir4 } from "node:os";
@@ -14816,7 +14862,7 @@ async function ensureHelperCompiled(directory = tmpdir4(), budgetMs = HELPER_COM
14816
14862
  async function compileHelper(directory, assembly, budgetMs) {
14817
14863
  const scratch = mkdtempSync2(join18(directory, "gdharness-desktop-source-"));
14818
14864
  const source = join18(scratch, "helper.cs");
14819
- writeFileSync8(source, SOURCE, "utf8");
14865
+ writeFileSync9(source, SOURCE, "utf8");
14820
14866
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14821
14867
  const script = [
14822
14868
  "$ErrorActionPreference = 'Stop'",
@@ -14902,9 +14948,10 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
14902
14948
  how.noActivate === true ? "1" : "0",
14903
14949
  files.stop,
14904
14950
  how.directory,
14905
- files.step
14951
+ files.step,
14952
+ how.affinity === undefined ? "" : how.affinity.toString()
14906
14953
  ];
14907
- writeFileSync8(files.spec, fields.map((field) => Buffer.from(field, "utf8").toString("base64")).join(`
14954
+ writeFileSync9(files.spec, fields.map((field) => Buffer.from(field, "utf8").toString("base64")).join(`
14908
14955
  `), "utf8");
14909
14956
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
14910
14957
  const script = [
@@ -14976,6 +15023,9 @@ public static class GdharnessDesktop {
14976
15023
  [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
14977
15024
  [DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr handle, uint code);
14978
15025
  [DllImport("kernel32.dll")] static extern uint SetErrorMode(uint mode);
15026
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetProcessAffinityMask(IntPtr process, UIntPtr mask);
15027
+ [DllImport("kernel32.dll")] static extern uint ResumeThread(IntPtr thread);
15028
+ const uint CreateSuspended = 0x4;
14979
15029
  const uint FailCriticalErrors = 0x1;
14980
15030
  const uint NoOpenFileErrorBox = 0x8000;
14981
15031
 
@@ -15037,7 +15087,8 @@ public static class GdharnessDesktop {
15037
15087
  public static int RunSpec(string specFile) {
15038
15088
  string[] lines = File.ReadAllLines(specFile);
15039
15089
  Func<int, string> field = delegate(int index) { return Encoding.UTF8.GetString(Convert.FromBase64String(lines[index])); };
15040
- return Run(field(0), field(1), field(2), field(3) == "1", field(4), field(5), field(6));
15090
+ string affinity = lines.Length > 7 ? field(7) : "";
15091
+ return Run(field(0), field(1), field(2), field(3) == "1", field(4), field(5), field(6), affinity.Length == 0 ? 0UL : ulong.Parse(affinity));
15041
15092
  }
15042
15093
 
15043
15094
  static void Step(string stepFile, string step) {
@@ -15046,7 +15097,9 @@ public static class GdharnessDesktop {
15046
15097
  }
15047
15098
  }
15048
15099
 
15049
- public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory, string stepFile) {
15100
+ // With an affinity, the process starts suspended and is held to those processors before its
15101
+ // first thread runs, so every thread it starts afterwards is held to them as well.
15102
+ public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory, string stepFile, ulong affinity) {
15050
15103
  StartupInfo startup = new StartupInfo();
15051
15104
  startup.cb = Marshal.SizeOf(typeof(StartupInfo));
15052
15105
  if (desktop.Length > 0) {
@@ -15076,9 +15129,19 @@ public static class GdharnessDesktop {
15076
15129
  }
15077
15130
  try {
15078
15131
  Step(stepFile, "starting the process");
15079
- if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, DetachedProcess, IntPtr.Zero, directory, ref startup, out started)) {
15132
+ uint creation = DetachedProcess | (affinity == 0 ? 0u : CreateSuspended);
15133
+ if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, creation, IntPtr.Zero, directory, ref startup, out started)) {
15080
15134
  throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW " + commandLine);
15081
15135
  }
15136
+ if (affinity != 0) {
15137
+ Step(stepFile, "holding the process to its processors");
15138
+ if (!SetProcessAffinityMask(started.process, new UIntPtr(affinity))) {
15139
+ int error = Marshal.GetLastWin32Error();
15140
+ TerminateProcess(started.process, 1);
15141
+ throw new Win32Exception(error, "SetProcessAffinityMask " + affinity);
15142
+ }
15143
+ ResumeThread(started.thread);
15144
+ }
15082
15145
  File.WriteAllText(pidFile, started.processId.ToString());
15083
15146
  if (noActivate) {
15084
15147
  HoldTheForeground(started);
@@ -15154,7 +15217,13 @@ function readRun(printed) {
15154
15217
  afterCase = [];
15155
15218
  };
15156
15219
  const leftOut = new Map;
15220
+ let engineVersion = null;
15157
15221
  for (const [at, line] of lines.entries()) {
15222
+ const banner = engineVersion === null ? BANNER.exec(line) : null;
15223
+ if (banner !== null) {
15224
+ engineVersion = banner[1] ?? null;
15225
+ continue;
15226
+ }
15158
15227
  const left = LEFT_OUT.exec(line);
15159
15228
  if (left !== null) {
15160
15229
  const key = `${left[1] ?? ""} > ${left[2] ?? ""}`;
@@ -15241,7 +15310,7 @@ function readRun(printed) {
15241
15310
  `);
15242
15311
  const starved = STARVED.flatMap((pattern) => [...joined.matchAll(pattern)].map((found) => found[0]));
15243
15312
  const unloadable = scriptErrorsPrinted(lines).map((error) => `${error.path}${error.line === null ? "" : `:${String(error.line)}`}: ${error.message}`);
15244
- return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn, leftOut };
15313
+ return { lines, suites, errors, leaks, starved, executed, unloadable, stoppedIn, leftOut, engineVersion };
15245
15314
  }
15246
15315
  function withNestedErrors(printed, piped) {
15247
15316
  const nested = piped.flatMap((entry, at) => {
@@ -15250,6 +15319,12 @@ function withNestedErrors(printed, piped) {
15250
15319
  });
15251
15320
  return nested.length === 0 ? printed : { ...printed, errors: [...printed.errors, ...nested] };
15252
15321
  }
15322
+ function versionFault(said, wanted) {
15323
+ if (said === null) {
15324
+ return `the engine printed no version as it started, so it cannot be told to be Godot ${wanted}`;
15325
+ }
15326
+ return said === wanted || said.startsWith(`${wanted}.`) ? null : `the engine that ran is Godot ${said}, not the ${wanted} --godot-version asks for`;
15327
+ }
15253
15328
  function readExpected(file) {
15254
15329
  const declared = (value) => typeof value === "object" && value !== null ? value : {};
15255
15330
  try {
@@ -15331,11 +15406,19 @@ function judgeEngine(printed, handed, end) {
15331
15406
  if (end.ended !== null) {
15332
15407
  faults.push(end.ended);
15333
15408
  } else if (end.code !== 0 && !explained) {
15334
- faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
15409
+ const free = end.freeBytes === undefined ? "" : `, with ${(end.freeBytes / 1024 ** 3).toFixed(1)} GB of memory free as it ended`;
15410
+ faults.push(end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}${free}` : `the engine exited ${exitNamed(end.code)}${free}`);
15411
+ }
15412
+ if (printed.lines.every((line) => line.trim() === "")) {
15413
+ faults.push("the engine printed nothing at all");
15335
15414
  }
15336
15415
  if (printed.unloadable.length > 0) {
15337
15416
  faults.push(`gdUnit4 could not load ${named(printed.unloadable)}, so it ran nothing`);
15338
15417
  }
15418
+ const wrongEngine = handed.godotVersion === undefined || handed.godotVersion === null ? null : versionFault(printed.engineVersion, handed.godotVersion);
15419
+ if (wrongEngine !== null) {
15420
+ faults.push(wrongEngine);
15421
+ }
15339
15422
  if (printed.executed === null || printed.executed === 0) {
15340
15423
  faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
15341
15424
  }
@@ -15489,7 +15572,7 @@ ${String(error.warning)}`);
15489
15572
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
15490
15573
  return { printed: unexpected, neverPrinted };
15491
15574
  }
15492
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, LEFT_OUT, DRIVER_FALLBACK, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
15575
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, BANNER, LEFT_OUT, DRIVER_FALLBACK, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_EXITS, TAIL = 60, MOST_NAMED = 8;
15493
15576
  var init_suite_judge = __esm(() => {
15494
15577
  init_engine_leaks();
15495
15578
  init_junit();
@@ -15500,6 +15583,7 @@ var init_suite_judge = __esm(() => {
15500
15583
  ERROR_LINE = /^\s*(?:SCRIPT |USER )?ERROR: (.*?)\s*$/;
15501
15584
  WARNING_LINE = /^\s*(?:SCRIPT |USER )?WARNING: (.*?)\s*$/;
15502
15585
  EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
15586
+ BANNER = /^\s*Godot Engine v(\S+?)(?:\s+-\s+\S+)?\s*$/;
15503
15587
  LEFT_OUT = /^gdharness left out (res:\/\/.+?\.gd) > ([^:\s]+):(-?\d+)\s*$/;
15504
15588
  DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
15505
15589
  ORPHANS_REPORTED = /^\s*WARNING: Detected \d+ (?:possible )?orphan nodes\.\s*$/;
@@ -15527,11 +15611,11 @@ var init_suite_judge = __esm(() => {
15527
15611
  });
15528
15612
 
15529
15613
  // src/whole-file.ts
15530
- import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "node:fs";
15614
+ import { renameSync as renameSync2, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "node:fs";
15531
15615
  function writeWhole(path, text) {
15532
15616
  const partial = `${path}.${process.pid}.partial`;
15533
15617
  try {
15534
- writeFileSync9(partial, text, "utf8");
15618
+ writeFileSync10(partial, text, "utf8");
15535
15619
  } catch {
15536
15620
  return false;
15537
15621
  }
@@ -15558,16 +15642,29 @@ var init_whole_file = __esm(() => {
15558
15642
  });
15559
15643
 
15560
15644
  // src/suite-plan.ts
15561
- import { existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync18 } from "node:fs";
15562
- import { dirname as dirname10, posix as posix3 } from "node:path";
15645
+ import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync18 } from "node:fs";
15646
+ import { dirname as dirname10, join as join21, posix as posix3 } from "node:path";
15647
+ function within(path, folder) {
15648
+ return path === folder || path.startsWith(`${folder}/`);
15649
+ }
15563
15650
 
15564
15651
  class ProjectScripts {
15565
15652
  sources = new Map;
15566
15653
  classes = new Map;
15567
15654
  read = new Map;
15568
15655
  projectPath;
15656
+ notSearched;
15657
+ guessWindow;
15569
15658
  constructor(projectPath) {
15570
15659
  this.projectPath = projectPath;
15660
+ let settings = {};
15661
+ try {
15662
+ settings = parseProjectGodot(readFileSync18(join21(projectPath, "project.godot"), "utf8"))["gdharness"] ?? {};
15663
+ } catch {}
15664
+ const [section, key] = [NOT_SEARCHED_SETTING, GUESS_WINDOW_SETTING].map((setting) => setting.slice("gdharness/".length));
15665
+ const listed = settings[section ?? ""];
15666
+ this.notSearched = typeof listed === "string" ? [...listed.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((found) => (found[1] ?? "").replace(/\/+$/, "")) : [];
15667
+ this.guessWindow = settings[key ?? ""] !== false;
15571
15668
  eachScript(projectPath, (script, source) => {
15572
15669
  this.sources.set(script, source);
15573
15670
  const declared = declaredClassOf(source);
@@ -15628,7 +15725,7 @@ class ProjectScripts {
15628
15725
  }
15629
15726
  const declared = NEEDS_WINDOW.exec(read.code)?.[1];
15630
15727
  said ??= declared === undefined ? undefined : declared === "true";
15631
- guessed ||= SCENE_RUNNER.test(read.code);
15728
+ guessed ||= this.guessWindow && SCENE_RUNNER.test(read.code);
15632
15729
  const base = this.base(at);
15633
15730
  if (base === null) {
15634
15731
  return null;
@@ -15644,8 +15741,16 @@ class ProjectScripts {
15644
15741
  return null;
15645
15742
  }
15646
15743
  suitesUnder(roots) {
15647
- const prefixes = roots.map((root) => root.endsWith("/") ? root : `${root}/`);
15648
- return [...this.sources.keys()].filter((path) => prefixes.some((prefix) => path.startsWith(prefix))).sort().flatMap((path) => this.suite(path) ?? []);
15744
+ const searched = roots.map((root) => root.replace(/\/+$/, ""));
15745
+ return [...this.sources.keys()].filter((path) => searched.some((root) => path.startsWith(`${root}/`) && !this.notSearched.some((folder) => within(path, folder) && !within(root, folder)))).sort().flatMap((path) => this.suite(path) ?? []);
15746
+ }
15747
+ searchedAsTold(paths) {
15748
+ return [
15749
+ ...new Set(paths.flatMap((path) => {
15750
+ const root = path.replace(/\/+$/, "");
15751
+ return path.endsWith(".gd") || !this.notSearched.some((folder) => folder.startsWith(`${root}/`) && !within(root, folder)) ? [path] : this.suitesUnder([root]).map((suite) => suite.path);
15752
+ }))
15753
+ ];
15649
15754
  }
15650
15755
  }
15651
15756
  function readTimes(file) {
@@ -15657,7 +15762,7 @@ function readTimes(file) {
15657
15762
  }
15658
15763
  }
15659
15764
  function writeJson(file, value) {
15660
- mkdirSync7(dirname10(file), { recursive: true });
15765
+ mkdirSync8(dirname10(file), { recursive: true });
15661
15766
  writeWhole(file, `${JSON.stringify(value, null, 1)}
15662
15767
  `);
15663
15768
  }
@@ -15866,8 +15971,7 @@ function planOf(bin, declared, windowed, lane, all) {
15866
15971
  lane
15867
15972
  };
15868
15973
  }
15869
- function windowNeedsOf(projectPath, paths) {
15870
- const scripts = new ProjectScripts(projectPath);
15974
+ function windowNeedsOf(scripts, paths) {
15871
15975
  const suites = new Map(paths.flatMap((path) => path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path])).map((suite) => [suite.path, suite]));
15872
15976
  const sorted = [...suites.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
15873
15977
  return {
@@ -15885,10 +15989,11 @@ function viewportOf(projectPath) {
15885
15989
  const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
15886
15990
  return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
15887
15991
  }
15888
- var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5;
15992
+ 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;
15889
15993
  var init_suite_plan = __esm(() => {
15890
15994
  init_class_cache();
15891
15995
  init_gdscript_source();
15996
+ init_resources();
15892
15997
  init_whole_file();
15893
15998
  NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
15894
15999
  TEST_DIRECTORIES = ["tests", "test"];
@@ -37497,12 +37602,12 @@ var init_stdio = __esm(() => {
37497
37602
 
37498
37603
  // src/autoload-failure.ts
37499
37604
  import { existsSync as existsSync20, readFileSync as readFileSync21 } from "node:fs";
37500
- import { join as join23 } from "node:path";
37605
+ import { join as join24 } from "node:path";
37501
37606
  function scriptsAutoloaded(projectPath) {
37502
37607
  const scripts = new Map;
37503
37608
  let settings;
37504
37609
  try {
37505
- settings = parseProjectGodot(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
37610
+ settings = parseProjectGodot(readFileSync21(join24(projectPath, "project.godot"), "utf8"));
37506
37611
  } catch {
37507
37612
  return scripts;
37508
37613
  }
@@ -37511,7 +37616,7 @@ function scriptsAutoloaded(projectPath) {
37511
37616
  scripts.set(`res://${path}`, name);
37512
37617
  continue;
37513
37618
  }
37514
- const scene = join23(projectPath, path);
37619
+ const scene = join24(projectPath, path);
37515
37620
  if (!existsSync20(scene)) {
37516
37621
  continue;
37517
37622
  }
@@ -37538,7 +37643,7 @@ var init_autoload_failure = __esm(() => {
37538
37643
  import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync22, statSync as statSync14, unlinkSync as unlinkSync2 } from "node:fs";
37539
37644
  import { createConnection } from "node:net";
37540
37645
  import { tmpdir as tmpdir5 } from "node:os";
37541
- import { dirname as dirname12, join as join24, resolve as resolve6 } from "node:path";
37646
+ import { dirname as dirname12, join as join25, resolve as resolve6 } from "node:path";
37542
37647
  import { setTimeout as delay4 } from "node:timers/promises";
37543
37648
  function runtimeDirectory(variables = process.env) {
37544
37649
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
@@ -37546,19 +37651,19 @@ function runtimeDirectory(variables = process.env) {
37546
37651
  return explicit;
37547
37652
  }
37548
37653
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
37549
- return join24(perUser ?? tmpdir5(), "gdharness");
37654
+ return join25(perUser ?? tmpdir5(), "gdharness");
37550
37655
  }
37551
37656
  function runtimeDirectories(variables = process.env) {
37552
37657
  const candidates = [runtimeDirectory(variables)];
37553
37658
  const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
37554
37659
  for (const base of fallbacks) {
37555
- candidates.push(join24(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
37660
+ candidates.push(join25(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
37556
37661
  }
37557
37662
  return [...new Set(candidates.map((path) => resolve6(path)))];
37558
37663
  }
37559
37664
  function errorReportOf(pid, directories = runtimeDirectories()) {
37560
37665
  for (const directory of directories) {
37561
- const path = join24(directory, `runtime-${pid}.log`);
37666
+ const path = join25(directory, `runtime-${pid}.log`);
37562
37667
  if (existsSync21(path)) {
37563
37668
  return path;
37564
37669
  }
@@ -37714,14 +37819,14 @@ function announcedIn(directory) {
37714
37819
  for (const entry of readdirSync13(directory)) {
37715
37820
  const report = ERROR_REPORT_PATTERN.exec(entry);
37716
37821
  if (report) {
37717
- sweepErrorReport(join24(directory, entry), Number.parseInt(report[1] ?? "", 10));
37822
+ sweepErrorReport(join25(directory, entry), Number.parseInt(report[1] ?? "", 10));
37718
37823
  continue;
37719
37824
  }
37720
37825
  const match = ANNOUNCEMENT_PATTERN.exec(entry);
37721
37826
  if (!match) {
37722
37827
  continue;
37723
37828
  }
37724
- const file = join24(directory, entry);
37829
+ const file = join25(directory, entry);
37725
37830
  const pid = Number.parseInt(match[1] ?? "", 10);
37726
37831
  const running = alive(pid);
37727
37832
  entries.push({ file, pid, running });
@@ -37749,7 +37854,7 @@ function announcedIn(directory) {
37749
37854
  found.push(announced);
37750
37855
  }
37751
37856
  const held = new Set(entries.map((one) => one.file));
37752
- const here = join24(directory, ".");
37857
+ const here = join25(directory, ".");
37753
37858
  for (const file of verdicts.keys()) {
37754
37859
  if (dirname12(file) === here && !held.has(file)) {
37755
37860
  verdicts.delete(file);
@@ -37758,7 +37863,7 @@ function announcedIn(directory) {
37758
37863
  return found;
37759
37864
  }
37760
37865
  async function announcementEnded(pid, directories = runtimeDirectories()) {
37761
- const files = directories.map((directory) => join24(directory, `runtime-${pid}.json`)).filter((file) => existsSync21(file));
37866
+ const files = directories.map((directory) => join25(directory, `runtime-${pid}.json`)).filter((file) => existsSync21(file));
37762
37867
  const first = files[0];
37763
37868
  if (first === undefined) {
37764
37869
  return;
@@ -38177,10 +38282,10 @@ var init_runtime_client = __esm(() => {
38177
38282
  });
38178
38283
 
38179
38284
  // src/boot-note.ts
38180
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync23, writeFileSync as writeFileSync11 } from "node:fs";
38181
- import { dirname as dirname13, join as join25 } from "node:path";
38285
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
38286
+ import { dirname as dirname13, join as join26 } from "node:path";
38182
38287
  function bootNotePath(projectPath) {
38183
- return join25(projectPath, ".godot", "gdharness-boot.json");
38288
+ return join26(projectPath, ".godot", "gdharness-boot.json");
38184
38289
  }
38185
38290
  function readScenes(projectPath) {
38186
38291
  try {
@@ -38200,7 +38305,7 @@ function readBootNote(projectPath, scene) {
38200
38305
  function writeBootNote(projectPath, scene, announcedAfterMs) {
38201
38306
  try {
38202
38307
  const path = bootNotePath(projectPath);
38203
- mkdirSync9(dirname13(path), { recursive: true });
38308
+ mkdirSync10(dirname13(path), { recursive: true });
38204
38309
  const scenes = {
38205
38310
  ...readScenes(projectPath),
38206
38311
  [scene]: {
@@ -38208,7 +38313,7 @@ function writeBootNote(projectPath, scene, announcedAfterMs) {
38208
38313
  at: new Date().toISOString()
38209
38314
  }
38210
38315
  };
38211
- writeFileSync11(path, JSON.stringify({ scenes }, null, 2), "utf8");
38316
+ writeFileSync12(path, JSON.stringify({ scenes }, null, 2), "utf8");
38212
38317
  } catch {}
38213
38318
  }
38214
38319
  function halfAsLongAgain(lastBootMs) {
@@ -38226,10 +38331,10 @@ var init_boot_note = __esm(() => {
38226
38331
  });
38227
38332
 
38228
38333
  // src/breakpoint-note.ts
38229
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync24, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
38230
- import { dirname as dirname14, isAbsolute as isAbsolute7, join as join26, relative as relative4 } from "node:path";
38334
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync24, rmSync as rmSync11, writeFileSync as writeFileSync13 } from "node:fs";
38335
+ import { dirname as dirname14, isAbsolute as isAbsolute7, join as join27, relative as relative4 } from "node:path";
38231
38336
  function breakpointNotePath(projectPath) {
38232
- return join26(projectPath, ".godot", "gdharness-breakpoints.json");
38337
+ return join27(projectPath, ".godot", "gdharness-breakpoints.json");
38233
38338
  }
38234
38339
  function writeBreakpointNote(projectPath, held) {
38235
38340
  const path = breakpointNotePath(projectPath);
@@ -38246,8 +38351,8 @@ function writeBreakpointNote(projectPath, held) {
38246
38351
  lines: [...one.lines]
38247
38352
  }))
38248
38353
  };
38249
- mkdirSync10(dirname14(path), { recursive: true });
38250
- writeFileSync12(path, `${JSON.stringify(note, null, 2)}
38354
+ mkdirSync11(dirname14(path), { recursive: true });
38355
+ writeFileSync13(path, `${JSON.stringify(note, null, 2)}
38251
38356
  `, "utf8");
38252
38357
  } catch (error) {
38253
38358
  console.error(`[SERVER] Could not record the breakpoints at ${path}: ${errorMessage(error)}`);
@@ -38270,7 +38375,7 @@ function readBreakpointNote(projectPath) {
38270
38375
  }
38271
38376
  const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
38272
38377
  if (lines.length > 0) {
38273
- held.push({ scriptPath: realAsFarAsItExists(join26(projectPath, script)), lines });
38378
+ held.push({ scriptPath: realAsFarAsItExists(join27(projectPath, script)), lines });
38274
38379
  }
38275
38380
  }
38276
38381
  return held;
@@ -38288,10 +38393,10 @@ var init_breakpoint_note = __esm(() => {
38288
38393
  });
38289
38394
 
38290
38395
  // src/class-note.ts
38291
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync25 } from "node:fs";
38292
- import { dirname as dirname15, join as join27 } from "node:path";
38396
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync25 } from "node:fs";
38397
+ import { dirname as dirname15, join as join28 } from "node:path";
38293
38398
  function classNotePath(projectPath) {
38294
- return join27(projectPath, ".godot", "gdharness-classes.json");
38399
+ return join28(projectPath, ".godot", "gdharness-classes.json");
38295
38400
  }
38296
38401
  function readClassNote(projectPath) {
38297
38402
  try {
@@ -38325,7 +38430,7 @@ function unfinishedScanIn(value) {
38325
38430
  function writeClassNote(projectPath, note) {
38326
38431
  const path = classNotePath(projectPath);
38327
38432
  try {
38328
- mkdirSync11(dirname15(path), { recursive: true });
38433
+ mkdirSync12(dirname15(path), { recursive: true });
38329
38434
  } catch {
38330
38435
  return;
38331
38436
  }
@@ -38338,9 +38443,9 @@ var init_class_note = __esm(() => {
38338
38443
 
38339
38444
  // src/config-pin.ts
38340
38445
  import { existsSync as existsSync22, readFileSync as readFileSync26 } from "node:fs";
38341
- import { join as join28 } from "node:path";
38446
+ import { join as join29 } from "node:path";
38342
38447
  function versionsNamedInConfig(projectPath) {
38343
- const config = join28(projectPath, ".mcp.json");
38448
+ const config = join29(projectPath, ".mcp.json");
38344
38449
  if (!existsSync22(config)) {
38345
38450
  return [];
38346
38451
  }
@@ -38367,16 +38472,16 @@ var init_config_pin = __esm(() => {
38367
38472
  });
38368
38473
 
38369
38474
  // src/cut-test-run.ts
38370
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync27, rmSync as rmSync12, writeFileSync as writeFileSync13 } from "node:fs";
38371
- import { dirname as dirname16, join as join29 } from "node:path";
38475
+ import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync27, rmSync as rmSync12, writeFileSync as writeFileSync14 } from "node:fs";
38476
+ import { dirname as dirname16, join as join30 } from "node:path";
38372
38477
  function keptAt(projectPath) {
38373
- return join29(projectPath, ".godot", "gdharness-reports", "cut-run.json");
38478
+ return join30(projectPath, ".godot", "gdharness-reports", "cut-run.json");
38374
38479
  }
38375
38480
  function keepCutTestRun(projectPath, run) {
38376
38481
  const path = keptAt(projectPath);
38377
38482
  try {
38378
- mkdirSync12(dirname16(path), { recursive: true });
38379
- writeFileSync13(path, `${JSON.stringify(run, null, 2)}
38483
+ mkdirSync13(dirname16(path), { recursive: true });
38484
+ writeFileSync14(path, `${JSON.stringify(run, null, 2)}
38380
38485
  `, "utf8");
38381
38486
  } catch {}
38382
38487
  }
@@ -39246,20 +39351,20 @@ var init_dap_client = __esm(() => {
39246
39351
  import {
39247
39352
  closeSync as closeSync4,
39248
39353
  existsSync as existsSync24,
39249
- mkdirSync as mkdirSync13,
39354
+ mkdirSync as mkdirSync14,
39250
39355
  openSync as openSync4,
39251
39356
  readFileSync as readFileSync28,
39252
39357
  readSync as readSync2,
39253
39358
  rmSync as rmSync13,
39254
39359
  statSync as statSync15,
39255
- writeFileSync as writeFileSync14
39360
+ writeFileSync as writeFileSync15
39256
39361
  } from "node:fs";
39257
- import { dirname as dirname17, join as join30 } from "node:path";
39362
+ import { dirname as dirname17, join as join31 } from "node:path";
39258
39363
  function editorLogPath(projectPath) {
39259
- return join30(projectPath, ".godot", "gdharness-editor.log");
39364
+ return join31(projectPath, ".godot", "gdharness-editor.log");
39260
39365
  }
39261
39366
  function editorLogNotePath(projectPath) {
39262
- return join30(projectPath, ".godot", "gdharness-editor.json");
39367
+ return join31(projectPath, ".godot", "gdharness-editor.json");
39263
39368
  }
39264
39369
  function readEditorLogNote(projectPath) {
39265
39370
  try {
@@ -39275,7 +39380,7 @@ function readEditorLogNote(projectPath) {
39275
39380
  function clearEditorLog(projectPath) {
39276
39381
  try {
39277
39382
  const path = editorLogPath(projectPath);
39278
- mkdirSync13(dirname17(path), { recursive: true });
39383
+ mkdirSync14(dirname17(path), { recursive: true });
39279
39384
  rmSync13(path, { force: true });
39280
39385
  rmSync13(editorLogNotePath(projectPath), { force: true });
39281
39386
  } catch {}
@@ -39283,8 +39388,8 @@ function clearEditorLog(projectPath) {
39283
39388
  function writeEditorLogNote(projectPath, pid, processStartedAt) {
39284
39389
  try {
39285
39390
  const path = editorLogNotePath(projectPath);
39286
- mkdirSync13(dirname17(path), { recursive: true });
39287
- writeFileSync14(path, JSON.stringify({
39391
+ mkdirSync14(dirname17(path), { recursive: true });
39392
+ writeFileSync15(path, JSON.stringify({
39288
39393
  pid,
39289
39394
  startedAt: new Date().toISOString(),
39290
39395
  ...processStartedAt === undefined ? {} : { processStartedAt }
@@ -39528,7 +39633,7 @@ var init_editor_log = __esm(() => {
39528
39633
  });
39529
39634
 
39530
39635
  // src/rename.ts
39531
- import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync29, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync15 } from "node:fs";
39636
+ import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync29, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync16 } from "node:fs";
39532
39637
  import { dirname as dirname18, posix as posix4 } from "node:path";
39533
39638
 
39534
39639
  class Script {
@@ -40244,9 +40349,9 @@ function applyRewrites(rewrites) {
40244
40349
  const done = [];
40245
40350
  try {
40246
40351
  for (const rewrite of rewrites) {
40247
- mkdirSync14(dirname18(rewrite.writeTo), { recursive: true });
40352
+ mkdirSync15(dirname18(rewrite.writeTo), { recursive: true });
40248
40353
  const staging = `${rewrite.writeTo}.gdharness-rename`;
40249
- writeFileSync15(staging, rewrite.after, "utf8");
40354
+ writeFileSync16(staging, rewrite.after, "utf8");
40250
40355
  renameSync3(staging, rewrite.writeTo);
40251
40356
  let movedUid = false;
40252
40357
  if (rewrite.writeTo !== rewrite.absolute) {
@@ -40261,7 +40366,7 @@ function applyRewrites(rewrites) {
40261
40366
  } catch (error) {
40262
40367
  for (const { rewrite, movedUid } of done.reverse()) {
40263
40368
  try {
40264
- writeFileSync15(rewrite.absolute, rewrite.before, "utf8");
40369
+ writeFileSync16(rewrite.absolute, rewrite.before, "utf8");
40265
40370
  if (rewrite.writeTo !== rewrite.absolute) {
40266
40371
  unlinkSync3(rewrite.writeTo);
40267
40372
  if (movedUid) {
@@ -40300,7 +40405,7 @@ var init_rename = __esm(() => {
40300
40405
  import { existsSync as existsSync26, realpathSync as realpathSync3 } from "node:fs";
40301
40406
  import { readFile, realpath } from "node:fs/promises";
40302
40407
  import { createConnection as createConnection3 } from "node:net";
40303
- import { dirname as dirname19, join as join31, resolve as resolve7 } from "node:path";
40408
+ import { dirname as dirname19, join as join32, resolve as resolve7 } from "node:path";
40304
40409
  import { setTimeout as delay6 } from "node:timers/promises";
40305
40410
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
40306
40411
  function initializeTimeoutMs() {
@@ -40978,7 +41083,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
40978
41083
  } catch {
40979
41084
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
40980
41085
  }
40981
- if (!existsSync26(join31(projectPath, "project.godot"))) {
41086
+ if (!existsSync26(join32(projectPath, "project.godot"))) {
40982
41087
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
40983
41088
  }
40984
41089
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -41138,8 +41243,8 @@ var init_lsp_client = __esm(() => {
41138
41243
  });
41139
41244
 
41140
41245
  // src/rename-symbol.ts
41141
- import { existsSync as existsSync27, mkdirSync as mkdirSync15, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync16 } from "node:fs";
41142
- import { basename as basename9, dirname as dirname20, join as join32, relative as relative5, resolve as resolve8 } from "node:path";
41246
+ import { existsSync as existsSync27, mkdirSync as mkdirSync16, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync14, writeFileSync as writeFileSync17 } from "node:fs";
41247
+ import { basename as basename9, dirname as dirname20, join as join33, relative as relative5, resolve as resolve8 } from "node:path";
41143
41248
  function resourcePathOf(projectPath, absolute) {
41144
41249
  const inside = relative5(onDiskSpelling(projectPath), onDiskSpelling(absolute));
41145
41250
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
@@ -41152,7 +41257,7 @@ function onDiskSpelling(path) {
41152
41257
  let at = resolve8(path);
41153
41258
  for (;; ) {
41154
41259
  try {
41155
- return join32(realpathSync4.native(at), ...missing.reverse());
41260
+ return join33(realpathSync4.native(at), ...missing.reverse());
41156
41261
  } catch {
41157
41262
  const parent = dirname20(at);
41158
41263
  if (parent === at) {
@@ -41581,15 +41686,15 @@ ${mention.kind}`;
41581
41686
  };
41582
41687
  }
41583
41688
  function writeLeftAloneDetail(projectPath, symbol, detail) {
41584
- const directory = join32(projectPath, ".godot", "gdharness-renames");
41585
- mkdirSync15(directory, { recursive: true });
41689
+ const directory = join33(projectPath, ".godot", "gdharness-renames");
41690
+ mkdirSync16(directory, { recursive: true });
41586
41691
  detailWrites += 1;
41587
- const path = join32(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
41588
- writeFileSync16(path, `${JSON.stringify(detail, null, 2)}
41692
+ const path = join33(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
41693
+ writeFileSync17(path, `${JSON.stringify(detail, null, 2)}
41589
41694
  `, "utf8");
41590
41695
  const kept = readdirSync14(directory).filter((name) => name.endsWith(".json")).sort().reverse();
41591
41696
  for (const old of kept.slice(10)) {
41592
- rmSync14(join32(directory, old), { force: true });
41697
+ rmSync14(join33(directory, old), { force: true });
41593
41698
  }
41594
41699
  return path;
41595
41700
  }
@@ -41631,17 +41736,17 @@ var init_rename_symbol = __esm(() => {
41631
41736
  // src/file-move.ts
41632
41737
  import {
41633
41738
  existsSync as existsSync28,
41634
- mkdirSync as mkdirSync16,
41739
+ mkdirSync as mkdirSync17,
41635
41740
  readdirSync as readdirSync15,
41636
41741
  readFileSync as readFileSync30,
41637
41742
  renameSync as renameSync4,
41638
41743
  statSync as statSync16,
41639
- writeFileSync as writeFileSync17
41744
+ writeFileSync as writeFileSync18
41640
41745
  } from "node:fs";
41641
- import { basename as basename10, dirname as dirname21, extname as extname3, join as join33, relative as relative6 } from "node:path";
41746
+ import { basename as basename10, dirname as dirname21, extname as extname3, join as join34, relative as relative6 } from "node:path";
41642
41747
  function filesUnder(directory) {
41643
41748
  return readdirSync15(directory, { withFileTypes: true }).flatMap((entry) => {
41644
- const path = join33(directory, entry.name);
41749
+ const path = join34(directory, entry.name);
41645
41750
  return entry.isDirectory() ? filesUnder(path) : [path];
41646
41751
  });
41647
41752
  }
@@ -41662,8 +41767,8 @@ function unseenByTheEngine(projectPath, path) {
41662
41767
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
41663
41768
  }
41664
41769
  if (at < steps.length - 1) {
41665
- directory = join33(directory, step);
41666
- if (existsSync28(join33(directory, ".gdignore"))) {
41770
+ directory = join34(directory, step);
41771
+ if (existsSync28(join34(directory, ".gdignore"))) {
41667
41772
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
41668
41773
  }
41669
41774
  }
@@ -41675,12 +41780,12 @@ function installedAddonOf(projectPath, path) {
41675
41780
  if (steps[0] !== "addons" || steps[1] === undefined) {
41676
41781
  return null;
41677
41782
  }
41678
- return existsSync28(join33(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
41783
+ return existsSync28(join34(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
41679
41784
  }
41680
41785
  function binaryResources(projectPath) {
41681
41786
  const found = [];
41682
41787
  const visit = (directory, prefix) => {
41683
- if (existsSync28(join33(directory, ".gdignore"))) {
41788
+ if (existsSync28(join34(directory, ".gdignore"))) {
41684
41789
  return;
41685
41790
  }
41686
41791
  for (const entry of readdirSync15(directory, { withFileTypes: true })) {
@@ -41689,7 +41794,7 @@ function binaryResources(projectPath) {
41689
41794
  }
41690
41795
  const path = `${prefix}${entry.name}`;
41691
41796
  if (entry.isDirectory()) {
41692
- visit(join33(directory, entry.name), `${path}/`);
41797
+ visit(join34(directory, entry.name), `${path}/`);
41693
41798
  } else if (/\.(scn|res)$/i.test(entry.name)) {
41694
41799
  found.push(path);
41695
41800
  }
@@ -41699,7 +41804,7 @@ function binaryResources(projectPath) {
41699
41804
  return found;
41700
41805
  }
41701
41806
  function targetPathOf(projectPath, absolute) {
41702
- const inside = relative6(onDiskSpelling(projectPath), join33(onDiskSpelling(dirname21(absolute)), basename10(absolute)));
41807
+ const inside = relative6(onDiskSpelling(projectPath), join34(onDiskSpelling(dirname21(absolute)), basename10(absolute)));
41703
41808
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
41704
41809
  return null;
41705
41810
  }
@@ -41754,7 +41859,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
41754
41859
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
41755
41860
  }
41756
41861
  if (!isDirectory) {
41757
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync28(join33(projectPath, path.slice("res://".length))));
41862
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync28(join34(projectPath, path.slice("res://".length))));
41758
41863
  if (owner !== undefined && owner !== null) {
41759
41864
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
41760
41865
  }
@@ -41788,12 +41893,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
41788
41893
  scripts,
41789
41894
  moves,
41790
41895
  named: [from],
41791
- exists: (path) => existsSync28(join33(projectPath, path.slice("res://".length)))
41896
+ exists: (path) => existsSync28(join34(projectPath, path.slice("res://".length)))
41792
41897
  }, { edits: [], mentions: [] });
41793
41898
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
41794
41899
  const binaries = [];
41795
41900
  for (const path of binaryResources(projectPath)) {
41796
- const bytes = readFileSync30(join33(projectPath, path.slice("res://".length)));
41901
+ const bytes = readFileSync30(join34(projectPath, path.slice("res://".length)));
41797
41902
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
41798
41903
  if (names !== undefined) {
41799
41904
  binaries.push({
@@ -41812,7 +41917,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
41812
41917
  }));
41813
41918
  const left = leftAloneReport(mentions);
41814
41919
  const rewrites = rewritesOf(plan, texts, null);
41815
- const absoluteOf = (path) => join33(projectPath, path.slice("res://".length));
41920
+ const absoluteOf = (path) => join34(projectPath, path.slice("res://".length));
41816
41921
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
41817
41922
  const moved = [...moves].map(([source, target]) => {
41818
41923
  const rewrite = rewritten.get(source);
@@ -41871,7 +41976,7 @@ function applyFileMove(outcome) {
41871
41976
  }
41872
41977
  const undo = [];
41873
41978
  const rename = (source, target) => {
41874
- mkdirSync16(dirname21(target), { recursive: true });
41979
+ mkdirSync17(dirname21(target), { recursive: true });
41875
41980
  renameSync4(source, target);
41876
41981
  undo.push(() => {
41877
41982
  renameSync4(target, source);
@@ -41880,10 +41985,10 @@ function applyFileMove(outcome) {
41880
41985
  try {
41881
41986
  for (const rewrite of outcome.inPlace) {
41882
41987
  const staging = `${rewrite.absolute}.gdharness-move`;
41883
- writeFileSync17(staging, rewrite.after, "utf8");
41988
+ writeFileSync18(staging, rewrite.after, "utf8");
41884
41989
  renameSync4(staging, rewrite.absolute);
41885
41990
  undo.push(() => {
41886
- writeFileSync17(rewrite.absolute, rewrite.before, "utf8");
41991
+ writeFileSync18(rewrite.absolute, rewrite.before, "utf8");
41887
41992
  });
41888
41993
  }
41889
41994
  if (outcome.directory !== null) {
@@ -41895,9 +42000,9 @@ function applyFileMove(outcome) {
41895
42000
  }
41896
42001
  if (file.text !== null) {
41897
42002
  const { before, after } = file.text;
41898
- writeFileSync17(file.absoluteTo, after, "utf8");
42003
+ writeFileSync18(file.absoluteTo, after, "utf8");
41899
42004
  undo.push(() => {
41900
- writeFileSync17(file.absoluteTo, before, "utf8");
42005
+ writeFileSync18(file.absoluteTo, before, "utf8");
41901
42006
  });
41902
42007
  }
41903
42008
  }
@@ -45599,16 +45704,16 @@ var DEBUG_PREFIX = "debug ";
45599
45704
  var init_keeper_channel = () => {};
45600
45705
 
45601
45706
  // src/lifecycle-log.ts
45602
- import { appendFileSync, mkdirSync as mkdirSync17, renameSync as renameSync5, statSync as statSync17 } from "node:fs";
45603
- import { dirname as dirname22, join as join34 } from "node:path";
45707
+ import { appendFileSync, mkdirSync as mkdirSync18, renameSync as renameSync5, statSync as statSync17 } from "node:fs";
45708
+ import { dirname as dirname22, join as join35 } from "node:path";
45604
45709
  import process14 from "node:process";
45605
45710
  function lifecycleLogPath() {
45606
- return join34(runtimeDirectory(), "servers.log");
45711
+ return join35(runtimeDirectory(), "servers.log");
45607
45712
  }
45608
45713
  function writeLifecycle(event, fields = {}) {
45609
45714
  const path = lifecycleLogPath();
45610
45715
  try {
45611
- mkdirSync17(dirname22(path), { recursive: true });
45716
+ mkdirSync18(dirname22(path), { recursive: true });
45612
45717
  if ((statSync17(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
45613
45718
  try {
45614
45719
  renameSync5(path, `${path}.1`);
@@ -45967,16 +46072,16 @@ var init_prompts = __esm(() => {
45967
46072
  });
45968
46073
 
45969
46074
  // src/restart-note.ts
45970
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync31, rmSync as rmSync15, writeFileSync as writeFileSync18 } from "node:fs";
45971
- import { dirname as dirname23, join as join35 } from "node:path";
46075
+ import { mkdirSync as mkdirSync19, readFileSync as readFileSync31, rmSync as rmSync15, writeFileSync as writeFileSync19 } from "node:fs";
46076
+ import { dirname as dirname23, join as join36 } from "node:path";
45972
46077
  function restartNotePath(projectPath) {
45973
- return join35(projectPath, ".godot", "gdharness-restart.json");
46078
+ return join36(projectPath, ".godot", "gdharness-restart.json");
45974
46079
  }
45975
46080
  function noteRestartBegun(note) {
45976
46081
  const path = restartNotePath(note.projectPath);
45977
46082
  try {
45978
- mkdirSync18(dirname23(path), { recursive: true });
45979
- writeFileSync18(path, `${JSON.stringify(note, null, 2)}
46083
+ mkdirSync19(dirname23(path), { recursive: true });
46084
+ writeFileSync19(path, `${JSON.stringify(note, null, 2)}
45980
46085
  `, "utf8");
45981
46086
  } catch (error) {
45982
46087
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -46010,10 +46115,10 @@ var init_restart_note = __esm(() => {
46010
46115
 
46011
46116
  // src/run-record.ts
46012
46117
  import { createHash as createHash7 } from "node:crypto";
46013
- import { mkdirSync as mkdirSync19, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync32, rmSync as rmSync16, statSync as statSync18 } from "node:fs";
46014
- import { basename as basename11, join as join36, resolve as resolve9 } from "node:path";
46118
+ import { mkdirSync as mkdirSync20, openSync as openSync5, readdirSync as readdirSync16, readFileSync as readFileSync32, rmSync as rmSync16, statSync as statSync18 } from "node:fs";
46119
+ import { basename as basename11, join as join37, resolve as resolve9 } from "node:path";
46015
46120
  function runsDirectory() {
46016
- return join36(runtimeDirectory(), "runs");
46121
+ return join37(runtimeDirectory(), "runs");
46017
46122
  }
46018
46123
  function projectKey(projectPath) {
46019
46124
  const real = realAsFarAsItExists(resolve9(projectPath)).replace(/[\\/]+$/, "");
@@ -46021,14 +46126,14 @@ function projectKey(projectPath) {
46021
46126
  return createHash7("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
46022
46127
  }
46023
46128
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
46024
- return join36(directory, "runs", `note-${projectKey(projectPath)}.json`);
46129
+ return join37(directory, "runs", `note-${projectKey(projectPath)}.json`);
46025
46130
  }
46026
46131
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
46027
- return join36(directory, "runs", `latest-${projectKey(servedBy)}.json`);
46132
+ return join37(directory, "runs", `latest-${projectKey(servedBy)}.json`);
46028
46133
  }
46029
46134
  function writeLatestRun(servedBy, latest) {
46030
46135
  try {
46031
- mkdirSync19(runsDirectory(), { recursive: true });
46136
+ mkdirSync20(runsDirectory(), { recursive: true });
46032
46137
  writeWhole(latestRunPath(servedBy), JSON.stringify(latest, null, 2));
46033
46138
  } catch {}
46034
46139
  }
@@ -46048,24 +46153,24 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
46048
46153
  }
46049
46154
  }
46050
46155
  function openTranscript(startedAt) {
46051
- mkdirSync19(runsDirectory(), { recursive: true });
46052
- const path = join36(runsDirectory(), `run-${startedAt}.log`);
46156
+ mkdirSync20(runsDirectory(), { recursive: true });
46157
+ const path = join37(runsDirectory(), `run-${startedAt}.log`);
46053
46158
  return { path, fd: openSync5(path, "a") };
46054
46159
  }
46055
46160
  function profilePath(startedAt) {
46056
- return join36(runsDirectory(), `run-${startedAt}.profile.json`);
46161
+ return join37(runsDirectory(), `run-${startedAt}.profile.json`);
46057
46162
  }
46058
46163
  function profileMarkPath(startedAt) {
46059
- return join36(runsDirectory(), `run-${startedAt}.profile-mark.json`);
46164
+ return join37(runsDirectory(), `run-${startedAt}.profile-mark.json`);
46060
46165
  }
46061
46166
  function noteIsAbout(note, playing) {
46062
46167
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
46063
46168
  }
46064
46169
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
46065
- return join36(directory, "runs", `editor-${projectKey(projectPath)}.json`);
46170
+ return join37(directory, "runs", `editor-${projectKey(projectPath)}.json`);
46066
46171
  }
46067
46172
  function writeEditorRunNote(note) {
46068
- mkdirSync19(runsDirectory(), { recursive: true });
46173
+ mkdirSync20(runsDirectory(), { recursive: true });
46069
46174
  const path = editorNotePath(note.projectPath);
46070
46175
  if (!writeWhole(path, JSON.stringify(note, null, 2))) {
46071
46176
  throw new Error(`the editor run note could not be written at ${path}`);
@@ -46115,13 +46220,13 @@ function everyRunRecord() {
46115
46220
  for (const directory of runtimeDirectories()) {
46116
46221
  let entries;
46117
46222
  try {
46118
- entries = readdirSync16(join36(directory, "runs"));
46223
+ entries = readdirSync16(join37(directory, "runs"));
46119
46224
  } catch {
46120
46225
  continue;
46121
46226
  }
46122
46227
  for (const entry of entries) {
46123
46228
  if (entry.startsWith("note-") && entry.endsWith(".json")) {
46124
- const record = recordAt(join36(directory, "runs", entry));
46229
+ const record = recordAt(join37(directory, "runs", entry));
46125
46230
  if (record !== null) {
46126
46231
  found.push(record);
46127
46232
  }
@@ -46389,7 +46494,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
46389
46494
  if (!transcript && !entry.endsWith(".partial")) {
46390
46495
  continue;
46391
46496
  }
46392
- const path = join36(directory, entry);
46497
+ const path = join37(directory, entry);
46393
46498
  if (kept.has(path) || ageMs(path, now) < keepMs) {
46394
46499
  continue;
46395
46500
  }
@@ -46639,18 +46744,18 @@ var init_server_instructions = __esm(() => {
46639
46744
 
46640
46745
  // src/test-selection.ts
46641
46746
  import { existsSync as existsSync29, readFileSync as readFileSync34 } from "node:fs";
46642
- import { basename as basename12, join as join37 } from "node:path";
46747
+ import { basename as basename12, join as join38 } from "node:path";
46643
46748
  function casesIn(projectPath, suite) {
46644
- const text = readFileSync34(join37(projectPath, suite.slice("res://".length)), "utf8");
46749
+ const text = readFileSync34(join38(projectPath, suite.slice("res://".length)), "utf8");
46645
46750
  return [...text.matchAll(/^func\s+(test_\w*)\s*\(/gm)].map((match) => match[1] ?? "");
46646
46751
  }
46647
46752
  function scriptsWithoutCases(projectPath, asked) {
46648
- return asked.filter((path) => path.endsWith(".gd") && existsSync29(join37(projectPath, path.slice("res://".length))) && casesIn(projectPath, path).length === 0);
46753
+ return asked.filter((path) => path.endsWith(".gd") && existsSync29(join38(projectPath, path.slice("res://".length))) && casesIn(projectPath, path).length === 0);
46649
46754
  }
46650
46755
  function suitesUnder(projectPath, asked) {
46651
46756
  const scripts = projectFiles(projectPath, [".gd"]).map((path) => `res://${path}`);
46652
46757
  return [
46653
- ...new Set(asked.flatMap((path) => path.endsWith(".gd") ? existsSync29(join37(projectPath, path.slice("res://".length))) ? [path] : [] : scripts.filter((script) => script.startsWith(`${path}/`))))
46758
+ ...new Set(asked.flatMap((path) => path.endsWith(".gd") ? existsSync29(join38(projectPath, path.slice("res://".length))) ? [path] : [] : scripts.filter((script) => script.startsWith(`${path}/`))))
46654
46759
  ];
46655
46760
  }
46656
46761
  function selectTests(projectPath, asked, given) {
@@ -46713,7 +46818,7 @@ import {
46713
46818
  copyFileSync,
46714
46819
  existsSync as existsSync30,
46715
46820
  fstatSync as fstatSync2,
46716
- mkdirSync as mkdirSync20,
46821
+ mkdirSync as mkdirSync21,
46717
46822
  openSync as openSync6,
46718
46823
  readdirSync as readdirSync17,
46719
46824
  readFileSync as readFileSync35,
@@ -46721,9 +46826,9 @@ import {
46721
46826
  realpathSync as realpathSync5,
46722
46827
  rmdirSync as rmdirSync2,
46723
46828
  statSync as statSync19,
46724
- writeFileSync as writeFileSync19
46829
+ writeFileSync as writeFileSync20
46725
46830
  } from "node:fs";
46726
- import { basename as basename13, dirname as dirname24, extname as extname4, isAbsolute as isAbsolute8, join as join38, normalize as normalize2, relative as relative7 } from "node:path";
46831
+ import { basename as basename13, dirname as dirname24, extname as extname4, isAbsolute as isAbsolute8, join as join39, normalize as normalize2, relative as relative7 } from "node:path";
46727
46832
  import { setTimeout as delay7 } from "node:timers/promises";
46728
46833
  import { fileURLToPath as fileURLToPath5 } from "node:url";
46729
46834
  function editorRestartTimeoutMs() {
@@ -46782,7 +46887,7 @@ function writtenFileRefusal(path, rule) {
46782
46887
  if (!existsSync30(directory)) {
46783
46888
  return `${named}'s directory ${dirname24(path)} does not exist; ${what} creates no directories.`;
46784
46889
  }
46785
- const where = join38(directory, basename13(path));
46890
+ const where = join39(directory, basename13(path));
46786
46891
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realAsFarAsItExists(project), where));
46787
46892
  if (inside !== undefined) {
46788
46893
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
@@ -47429,7 +47534,7 @@ function notRunNote(notRun, failFast) {
47429
47534
  function presetsIn(projectPath) {
47430
47535
  let text;
47431
47536
  try {
47432
- text = readFileSync35(join38(projectPath, "export_presets.cfg"), "utf8");
47537
+ text = readFileSync35(join39(projectPath, "export_presets.cfg"), "utf8");
47433
47538
  } catch {
47434
47539
  return null;
47435
47540
  }
@@ -47510,7 +47615,7 @@ function filesIn(folder) {
47510
47615
  try {
47511
47616
  for (const entry of readdirSync17(folder, { withFileTypes: true })) {
47512
47617
  if (entry.isFile()) {
47513
- found.set(entry.name, statSync19(join38(folder, entry.name)).mtimeMs);
47618
+ found.set(entry.name, statSync19(join39(folder, entry.name)).mtimeMs);
47514
47619
  }
47515
47620
  }
47516
47621
  } catch {}
@@ -47550,7 +47655,7 @@ function unsavedInEditorNote(scripts) {
47550
47655
  return `${named} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
47551
47656
  }
47552
47657
  function editorAddonMissing(projectPath) {
47553
- if (!existsSync30(join38(projectPath, "project.godot"))) {
47658
+ if (!existsSync30(join39(projectPath, "project.godot"))) {
47554
47659
  return null;
47555
47660
  }
47556
47661
  const report = inspectProject(projectPath);
@@ -47578,7 +47683,7 @@ function rewriteBusNamed(projectPath, from, to) {
47578
47683
  const giving = [`bus = &"${quoted(from)}"`, `bus = "${quoted(from)}"`];
47579
47684
  const rewritten = [];
47580
47685
  for (const file of projectFiles(projectPath, [".tscn", ".tres"])) {
47581
- const absolute = join38(projectPath, file);
47686
+ const absolute = join39(projectPath, file);
47582
47687
  const lines = readFileSync35(absolute, "utf8").split(`
47583
47688
  `);
47584
47689
  const at = [];
@@ -47591,14 +47696,14 @@ function rewriteBusNamed(projectPath, from, to) {
47591
47696
  return `bus = &"${quoted(to)}"${ending}`;
47592
47697
  });
47593
47698
  if (at.length > 0) {
47594
- writeFileSync19(absolute, after.join(`
47699
+ writeFileSync20(absolute, after.join(`
47595
47700
  `));
47596
47701
  rewritten.push({ file: `res://${file}`, lines: at });
47597
47702
  }
47598
47703
  }
47599
47704
  const namedInScripts = [];
47600
47705
  for (const file of projectFiles(projectPath, [".gd"])) {
47601
- readFileSync35(join38(projectPath, file), "utf8").split(`
47706
+ readFileSync35(join39(projectPath, file), "utf8").split(`
47602
47707
  `).forEach((line, index) => {
47603
47708
  if (line.includes(`"${from}"`)) {
47604
47709
  namedInScripts.push({ file: `res://${file}`, line: index + 1, text: line.trim() });
@@ -47873,7 +47978,7 @@ function profileFrameFunctions(projectPath) {
47873
47978
  let projectGodot = null;
47874
47979
  if (projectPath !== null) {
47875
47980
  try {
47876
- projectGodot = readFileSync35(join38(projectPath, "project.godot"), "utf8");
47981
+ projectGodot = readFileSync35(join39(projectPath, "project.godot"), "utf8");
47877
47982
  } catch {}
47878
47983
  }
47879
47984
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -48056,7 +48161,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
48056
48161
  }
48057
48162
  function gdUnitFailsOnScriptErrors(projectPath) {
48058
48163
  try {
48059
- return settingKeys(readFileSync35(join38(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
48164
+ return settingKeys(readFileSync35(join39(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
48060
48165
  } catch {
48061
48166
  return true;
48062
48167
  }
@@ -48071,7 +48176,7 @@ function uncaughtNote(uncaught) {
48071
48176
  }
48072
48177
  function elsewhereIn(projectPath, asked) {
48073
48178
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
48074
- if (wanted === "" || existsSync30(join38(projectPath, wanted))) {
48179
+ if (wanted === "" || existsSync30(join39(projectPath, wanted))) {
48075
48180
  return "";
48076
48181
  }
48077
48182
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -48094,7 +48199,7 @@ function windowNoteFor(needing, sharing) {
48094
48199
  return `${named(needing)} ${one ? "needs" : "need"} a window, as gdharness test reads ${one ? "it" : "them"}, so the run had one: the engine ran without --headless, at the size project.godot gives its viewport, with no sound${process.platform === "win32" ? ", on a desktop of its own, where its window neither shows nor takes the keyboard" : ""}. ${why}${shared}`;
48095
48200
  }
48096
48201
  function testPathsIn(projectPath, given) {
48097
- const defaults = TEST_DIRECTORIES.filter((directory) => existsSync30(join38(projectPath, directory)));
48202
+ const defaults = TEST_DIRECTORIES.filter((directory) => existsSync30(join39(projectPath, directory)));
48098
48203
  const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
48099
48204
  const listed = Array.isArray(entries) ? entries : [entries];
48100
48205
  if (listed.length === 0) {
@@ -48206,7 +48311,7 @@ function needsItsOwnProcess(asked) {
48206
48311
  class GodotServer {
48207
48312
  stdio = null;
48208
48313
  locator = new GodotLocator;
48209
- operationsScript = join38(__dirname2, "godot", "operations", "godot_operations.gd");
48314
+ operationsScript = join39(__dirname2, "godot", "operations", "godot_operations.gd");
48210
48315
  exportOptionsRead = new Map;
48211
48316
  heldWhilePending = new Set;
48212
48317
  evaluatedWhilePending = new Set;
@@ -48477,7 +48582,7 @@ class GodotServer {
48477
48582
  }
48478
48583
  noteTheStart() {
48479
48584
  const cwd = process.cwd();
48480
- const mcpJson = join38(cwd, ".mcp.json");
48585
+ const mcpJson = join39(cwd, ".mcp.json");
48481
48586
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
48482
48587
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
48483
48588
  writeLifecycle("started", {
@@ -48625,7 +48730,7 @@ class GodotServer {
48625
48730
  return checked.response;
48626
48731
  }
48627
48732
  const named = args["projectPath"];
48628
- if (typeof named === "string" && existsSync30(join38(named, "project.godot"))) {
48733
+ if (typeof named === "string" && existsSync30(join39(named, "project.godot"))) {
48629
48734
  this.lastProjectPath = named;
48630
48735
  }
48631
48736
  this.updates.refresh();
@@ -48661,8 +48766,8 @@ class GodotServer {
48661
48766
  let notSaved;
48662
48767
  try {
48663
48768
  savedAt = answer.content.map((item, index) => {
48664
- const file = join38(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
48665
- writeFileSync19(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
48769
+ const file = join39(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
48770
+ writeFileSync20(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
48666
48771
  return file;
48667
48772
  });
48668
48773
  } catch (error) {
@@ -49345,7 +49450,7 @@ class GodotServer {
49345
49450
  if (path === undefined) {
49346
49451
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
49347
49452
  }
49348
- const file = join38(path, "project.godot");
49453
+ const file = join39(path, "project.godot");
49349
49454
  if (!existsSync30(file)) {
49350
49455
  return {
49351
49456
  ok: false,
@@ -50214,7 +50319,7 @@ class GodotServer {
50214
50319
  const platform = presetPlatform(project.value.path, preset);
50215
50320
  if (platform !== null) {
50216
50321
  const known = await this.exportOptions(platform, {
50217
- presetsFile: join38(project.value.path, "export_presets.cfg"),
50322
+ presetsFile: join39(project.value.path, "export_presets.cfg"),
50218
50323
  preset,
50219
50324
  asked: options
50220
50325
  });
@@ -50265,7 +50370,7 @@ ${platform}`;
50265
50370
  }
50266
50371
  const empty = scratchDirectory("export");
50267
50372
  try {
50268
- writeFileSync19(join38(empty, "project.godot"), `config_version=5
50373
+ writeFileSync20(join39(empty, "project.godot"), `config_version=5
50269
50374
  `);
50270
50375
  const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE, editor: true }, "export_options", { platform, ...standing }, empty);
50271
50376
  const options = outcome.ok ? outcome.payload["options"] : undefined;
@@ -50304,7 +50409,7 @@ ${platform}`;
50304
50409
  }
50305
50410
  const folder = dirname24(output.absolutePath);
50306
50411
  try {
50307
- mkdirSync20(folder, { recursive: true });
50412
+ mkdirSync21(folder, { recursive: true });
50308
50413
  } catch (error) {
50309
50414
  return this.createErrorResponse(`Cannot create ${dirname24(output.relativePath)} to export into: ${errorMessage(error)}`);
50310
50415
  }
@@ -50321,7 +50426,7 @@ ${platform}`;
50321
50426
  const exportArgs = [
50322
50427
  "--headless",
50323
50428
  "--log-file",
50324
- join38(exportLogs, "engine.log"),
50429
+ join39(exportLogs, "engine.log"),
50325
50430
  "--path",
50326
50431
  project.value.path,
50327
50432
  pack ? "--export-pack" : debug ? "--export-debug" : "--export-release",
@@ -50412,8 +50517,9 @@ ${platform}`;
50412
50517
  if (movie !== null) {
50413
50518
  return this.createErrorResponse(movie);
50414
50519
  }
50415
- const missing = paths.value.filter((path) => !existsSync30(join38(project.value.path, path.slice("res://".length))));
50416
- const asked = paths.value.filter((path) => !missing.includes(path));
50520
+ const missing = paths.value.filter((path) => !existsSync30(join39(project.value.path, path.slice("res://".length))));
50521
+ const projectScripts = new ProjectScripts(project.value.path);
50522
+ const asked = projectScripts.searchedAsTold(paths.value.filter((path) => !missing.includes(path)));
50417
50523
  const unusable = gdUnit4Unusable(project.value.path);
50418
50524
  if (asked.length === 0) {
50419
50525
  const verdict = `nothing at ${missing.join(" and ")}`;
@@ -50452,7 +50558,7 @@ ${platform}`;
50452
50558
  return this.createErrorResponse(selection.reason);
50453
50559
  }
50454
50560
  const leftOut = new Set(readStringArray(args, "ignore") ?? []);
50455
- const needs = windowNeedsOf(project.value.path, selection.value?.suites ?? asked);
50561
+ const needs = windowNeedsOf(projectScripts, selection.value?.suites ?? asked);
50456
50562
  const needing = needs.needing.filter((path) => !leftOut.has(basename13(path, ".gd")));
50457
50563
  const noDisplay = resolveHeadless(undefined, { platform: process.platform, variables: process.env });
50458
50564
  const windowed = needing.length > 0 && !noDisplay;
@@ -50484,14 +50590,14 @@ ${platform}`;
50484
50590
  const reports = `res://.godot/${ours}`;
50485
50591
  const ignored = readStringArray(args, "ignore") ?? [];
50486
50592
  const userData = scratchDirectory("tests");
50487
- const suitesFile = join38(userData, "suites.txt");
50488
- const skippingFile = join38(userData, "skipping.txt");
50489
- const engineLog = join38(userData, "engine.log");
50490
- const expectedFile = join38(userData, "expected.json");
50491
- writeFileSync19(suitesFile, `${(selection.value?.suites ?? asked).join(`
50593
+ const suitesFile = join39(userData, "suites.txt");
50594
+ const skippingFile = join39(userData, "skipping.txt");
50595
+ const engineLog = join39(userData, "engine.log");
50596
+ const expectedFile = join39(userData, "expected.json");
50597
+ writeFileSync20(suitesFile, `${(selection.value?.suites ?? asked).join(`
50492
50598
  `)}
50493
50599
  `);
50494
- writeFileSync19(skippingFile, `${[...ignored, ...selection.value?.ignore ?? []].join(`
50600
+ writeFileSync20(skippingFile, `${[...ignored, ...selection.value?.ignore ?? []].join(`
50495
50601
  `)}
50496
50602
  `);
50497
50603
  const viewport = windowed ? viewportOf(project.value.path) : null;
@@ -50528,11 +50634,11 @@ ${platform}`;
50528
50634
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
50529
50635
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
50530
50636
  const helperFiles = {
50531
- spec: join38(userData, "spec"),
50532
- pid: join38(userData, "pid"),
50533
- error: join38(userData, "error"),
50534
- stop: join38(userData, "stop"),
50535
- step: join38(userData, "step")
50637
+ spec: join39(userData, "spec"),
50638
+ pid: join39(userData, "pid"),
50639
+ error: join39(userData, "error"),
50640
+ stop: join39(userData, "stop"),
50641
+ step: join39(userData, "step")
50536
50642
  };
50537
50643
  const onDesktop = windowed && process.platform === "win32";
50538
50644
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
@@ -50583,12 +50689,12 @@ ${platform}`;
50583
50689
  await run.settled;
50584
50690
  const helperFailed = onDesktop ? helperFailure(helperFiles.error) ?? (existsSync30(helperFiles.pid) ? null : `it never started, and ${helperStep(helperFiles.step)}`) : null;
50585
50691
  if (helperFailed !== null) {
50586
- discard(join38(project.value.path, ".godot", ...ours.split("/")), userData);
50692
+ discard(join39(project.value.path, ".godot", ...ours.split("/")), userData);
50587
50693
  return this.createErrorResponse(`No tests ran: ${named(needing)} ${needing.length === 1 ? "needs" : "need"} a window, and the engine could not be started on a desktop of its own: ${helperFailed}`);
50588
50694
  }
50589
50695
  const tookMs = Date.now() - began;
50590
50696
  const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
50591
- const reportsDir = join38(project.value.path, ".godot", ...ours.split("/"));
50697
+ const reportsDir = join39(project.value.path, ".godot", ...ours.split("/"));
50592
50698
  let report = null;
50593
50699
  let reportProblem = null;
50594
50700
  const casePrinted = withNestedErrors(readRun(existsSync30(engineLog) ? readFileSync35(engineLog, "utf8") : ""), run.log.all);
@@ -50597,7 +50703,7 @@ ${platform}`;
50597
50703
  const written = existsSync30(reportsDir) && notCompiled === null ? readdirSync17(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
50598
50704
  const newest = written.at(-1);
50599
50705
  if (newest !== undefined) {
50600
- report = parseJUnit(readFileSync35(join38(reportsDir, newest, "results.xml"), "utf8"));
50706
+ report = parseJUnit(readFileSync35(join39(reportsDir, newest, "results.xml"), "utf8"));
50601
50707
  }
50602
50708
  } catch (error) {
50603
50709
  reportProblem = errorMessage(error);
@@ -51339,7 +51445,7 @@ ${error.text}`;
51339
51445
  ...Array.isArray(stack) ? {
51340
51446
  stack: stack.map((frame) => ({
51341
51447
  ...frame,
51342
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join38(projectPath, frame["script"].slice("res://".length)) } : {}
51448
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join39(projectPath, frame["script"].slice("res://".length)) } : {}
51343
51449
  }))
51344
51450
  } : {}
51345
51451
  });
@@ -51668,7 +51774,7 @@ ${error.text}`;
51668
51774
  return null;
51669
51775
  }
51670
51776
  try {
51671
- return settingKeys(readFileSync35(join38(projectPath, "project.godot"), "utf8"));
51777
+ return settingKeys(readFileSync35(join39(projectPath, "project.godot"), "utf8"));
51672
51778
  } catch {
51673
51779
  return null;
51674
51780
  }
@@ -52120,7 +52226,7 @@ ${error.text}`;
52120
52226
  }
52121
52227
  async runtimeUp(projectPath, before, budget, withArgs) {
52122
52228
  const budgetMs = budget.ms;
52123
- if (!existsSync30(join38(projectPath, RUNTIME_AUTOLOAD.path))) {
52229
+ if (!existsSync30(join39(projectPath, RUNTIME_AUTOLOAD.path))) {
52124
52230
  return runtimeVerdict(null, {
52125
52231
  addon: false,
52126
52232
  budgetMs,
@@ -52592,7 +52698,7 @@ ${error.text}`;
52592
52698
  if (onDesktop === undefined) {
52593
52699
  child.kill();
52594
52700
  } else {
52595
- writeFileSync19(onDesktop.files.stop, "");
52701
+ writeFileSync20(onDesktop.files.stop, "");
52596
52702
  }
52597
52703
  };
52598
52704
  const cancelled = callSignal();
@@ -53119,7 +53225,7 @@ ${error.text}`;
53119
53225
  };
53120
53226
  const going = await this.runStillGoing(run);
53121
53227
  const projectPath = run.projectPath;
53122
- const projectGone = projectPath !== null && !existsSync30(join38(projectPath, "project.godot"));
53228
+ const projectGone = projectPath !== null && !existsSync30(join39(projectPath, "project.godot"));
53123
53229
  if (projectGone && callersName !== undefined) {
53124
53230
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
53125
53231
  }
@@ -53504,7 +53610,7 @@ ${error.text}`;
53504
53610
  notes.push(endedWithoutACode({
53505
53611
  throughEditor: run.throughEditor,
53506
53612
  announced: announced !== undefined || run.announcedPid !== undefined,
53507
- couldAnnounce: run.projectPath !== null && existsSync30(join38(run.projectPath, RUNTIME_AUTOLOAD.path)),
53613
+ couldAnnounce: run.projectPath !== null && existsSync30(join39(run.projectPath, RUNTIME_AUTOLOAD.path)),
53508
53614
  endedHere: typeof run.endedHere === "string",
53509
53615
  pickedUp: run.pickedUp === true
53510
53616
  }));
@@ -54921,7 +55027,7 @@ ${error.text}`;
54921
55027
  async ownRunOnItsWay(projectPath, budgetMs) {
54922
55028
  const run = await this.currentRun();
54923
55029
  const project = projectPath ?? run?.projectPath ?? null;
54924
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync30(join38(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
55030
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync30(join39(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
54925
55031
  return null;
54926
55032
  }
54927
55033
  const already = runtimesAnnounced().running;
@@ -55031,7 +55137,7 @@ ${error.text}`;
55031
55137
  return this.createErrorResponse(staleNote);
55032
55138
  }
55033
55139
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
55034
- const screenshotPath = screenshotDir ? join38(screenshotDir, "capture.png") : null;
55140
+ const screenshotPath = screenshotDir ? join39(screenshotDir, "capture.png") : null;
55035
55141
  const refused = (message) => this.createErrorResponse(message);
55036
55142
  const onGame = {
55037
55143
  pid: choice.endpoint.pid,
@@ -55366,7 +55472,7 @@ var init_server = __esm(() => {
55366
55472
  // src/cli.ts
55367
55473
  init_bridge_announce();
55368
55474
  import { existsSync as existsSync31 } from "node:fs";
55369
- import { join as join39, resolve as resolve10 } from "node:path";
55475
+ import { join as join40, resolve as resolve10 } from "node:path";
55370
55476
 
55371
55477
  // src/error-mode.ts
55372
55478
  import process3 from "node:process";
@@ -55391,9 +55497,6 @@ async function letWindowsReportCrashes() {
55391
55497
  }
55392
55498
  } catch {}
55393
55499
  }
55394
- function crashesReported() {
55395
- return cleared;
55396
- }
55397
55500
 
55398
55501
  // src/cli.ts
55399
55502
  init_errors();
@@ -56363,7 +56466,7 @@ init_setup();
56363
56466
  init_setup();
56364
56467
  init_tool_definitions();
56365
56468
  init_tool_reference();
56366
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
56469
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync as readdirSync10, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
56367
56470
  import { basename as basename8, dirname as dirname9, join as join17 } from "node:path";
56368
56471
  var SKILL_NAME = "gdharness";
56369
56472
  var SHARED_SKILLS = join17(".agents", "skills");
@@ -56658,8 +56761,8 @@ function writeSkill(directories, version) {
56658
56761
  rmSync6(directory, { recursive: true, force: true });
56659
56762
  for (const [name, contents] of files) {
56660
56763
  const path = join17(directory, name);
56661
- mkdirSync5(dirname9(path), { recursive: true });
56662
- writeFileSync7(path, contents, "utf8");
56764
+ mkdirSync6(dirname9(path), { recursive: true });
56765
+ writeFileSync8(path, contents, "utf8");
56663
56766
  }
56664
56767
  return { path: directory, replaced };
56665
56768
  });
@@ -56667,30 +56770,31 @@ function writeSkill(directories, version) {
56667
56770
 
56668
56771
  // src/suite-run.ts
56669
56772
  init_alive();
56773
+ init_class_cache();
56670
56774
  init_desktop();
56671
56775
  init_engine_leaks();
56672
56776
  import {
56673
56777
  closeSync as closeSync3,
56674
56778
  existsSync as existsSync19,
56675
56779
  fstatSync,
56676
- mkdirSync as mkdirSync8,
56780
+ mkdirSync as mkdirSync9,
56677
56781
  openSync as openSync3,
56678
56782
  readdirSync as readdirSync12,
56679
56783
  readFileSync as readFileSync20,
56680
56784
  readSync,
56681
56785
  rmSync as rmSync10,
56682
56786
  statSync as statSync13,
56683
- writeFileSync as writeFileSync10
56787
+ writeFileSync as writeFileSync11
56684
56788
  } from "node:fs";
56685
56789
  import { availableParallelism, freemem, totalmem } from "node:os";
56686
- import { isAbsolute as isAbsolute6, join as join22, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
56790
+ import { isAbsolute as isAbsolute6, join as join23, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
56687
56791
  import process12 from "node:process";
56688
56792
  import { setTimeout as delay3 } from "node:timers/promises";
56689
56793
 
56690
56794
  // src/engine-slots.ts
56691
56795
  init_alive();
56692
56796
  init_process_children();
56693
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync15, rmSync as rmSync8, statSync as statSync11, writeSync as writeSync2 } from "node:fs";
56797
+ import { closeSync as closeSync2, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync15, rmSync as rmSync8, statSync as statSync11, writeSync as writeSync2 } from "node:fs";
56694
56798
  import { join as join19 } from "node:path";
56695
56799
  import process11 from "node:process";
56696
56800
  import { setTimeout as delay2 } from "node:timers/promises";
@@ -56738,7 +56842,7 @@ class EngineSlots {
56738
56842
  this.holder = holder;
56739
56843
  }
56740
56844
  async take(signal) {
56741
- mkdirSync6(this.directory, { recursive: true });
56845
+ mkdirSync7(this.directory, { recursive: true });
56742
56846
  for (;; ) {
56743
56847
  signal?.throwIfAborted();
56744
56848
  for (let slot = 0;slot < this.count; slot += 1) {
@@ -56832,6 +56936,8 @@ class EngineSlots {
56832
56936
 
56833
56937
  // src/suite-run.ts
56834
56938
  init_errors();
56939
+ init_import_freshness();
56940
+ init_library_copies();
56835
56941
  init_setup();
56836
56942
  init_spawn();
56837
56943
 
@@ -56977,13 +57083,13 @@ init_suite_plan();
56977
57083
 
56978
57084
  // src/version.ts
56979
57085
  import { readFileSync as readFileSync19 } from "node:fs";
56980
- import { dirname as dirname11, join as join21 } from "node:path";
57086
+ import { dirname as dirname11, join as join22 } from "node:path";
56981
57087
  import { fileURLToPath as fileURLToPath2 } from "node:url";
56982
57088
  function getLocalVersion() {
56983
57089
  const currentDirectory = dirname11(fileURLToPath2(import.meta.url));
56984
57090
  const packageCandidates = [
56985
- join21(currentDirectory, "..", "package.json"),
56986
- join21(currentDirectory, "..", "..", "package.json")
57091
+ join22(currentDirectory, "..", "package.json"),
57092
+ join22(currentDirectory, "..", "..", "package.json")
56987
57093
  ];
56988
57094
  for (const packagePath of packageCandidates) {
56989
57095
  try {
@@ -57001,6 +57107,8 @@ init_windows_ask();
57001
57107
 
57002
57108
  class TestUsageError extends Refusal {
57003
57109
  }
57110
+ var WINDOWS_EXCEPTION = 3221225472;
57111
+ var SLOWEST_NAMED = 5;
57004
57112
  var MOST_ENGINES = 4;
57005
57113
  var MEMORY_KEPT = 4 * 1024 ** 3;
57006
57114
  var MEMORY_PER_ENGINE = 1.5 * 1024 ** 3;
@@ -57015,6 +57123,10 @@ var VALUED = new Set([
57015
57123
  "--slots",
57016
57124
  "--slot-count",
57017
57125
  "--silence",
57126
+ "--deadline",
57127
+ "--godot",
57128
+ "--godot-version",
57129
+ "--cores",
57018
57130
  "--tests",
57019
57131
  "--reach-also",
57020
57132
  "--order"
@@ -57043,9 +57155,9 @@ function testOptionsOf(args, cwd) {
57043
57155
  }
57044
57156
  const first = positional[0];
57045
57157
  const named = first === undefined ? null : resolve5(cwd, first);
57046
- const projectNamed = named !== null && existsSync19(join22(named, "project.godot"));
57158
+ const projectNamed = named !== null && existsSync19(join23(named, "project.godot"));
57047
57159
  const projectPath = projectNamed ? named : resolve5(cwd);
57048
- if (!existsSync19(join22(projectPath, "project.godot"))) {
57160
+ if (!existsSync19(join23(projectPath, "project.godot"))) {
57049
57161
  throw new TestUsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
57050
57162
  }
57051
57163
  const last = (flag) => valued.get(flag)?.at(-1);
@@ -57057,6 +57169,10 @@ function testOptionsOf(args, cwd) {
57057
57169
  slotsDirectory: last("--slots") === undefined ? null : resolve5(cwd, last("--slots") ?? ""),
57058
57170
  slotCount: whole(last("--slot-count"), "--slot-count"),
57059
57171
  silenceSeconds: whole(last("--silence"), "--silence") ?? SILENCE_FLOOR_SECONDS,
57172
+ deadlineSeconds: whole(last("--deadline"), "--deadline"),
57173
+ godotPath: godotNamed(last("--godot"), cwd),
57174
+ godotVersion: versionNamed(last("--godot-version")),
57175
+ cores: coresNamed(last("--cores")),
57060
57176
  roots: (valued.get("--tests") ?? []).map((root) => resPathOf(root, projectPath, cwd).replace(/\/$/, "")),
57061
57177
  changedOnly: switches.has("--changed-only"),
57062
57178
  reachAlso: (valued.get("--reach-also") ?? []).map((path) => resolve5(projectPath, path)),
@@ -57075,6 +57191,34 @@ function whole(value, flag) {
57075
57191
  }
57076
57192
  return number;
57077
57193
  }
57194
+ function godotNamed(value, cwd) {
57195
+ if (value === undefined) {
57196
+ return null;
57197
+ }
57198
+ const path = resolve5(cwd, value);
57199
+ if (!existsSync19(path) || statSync13(path).isDirectory()) {
57200
+ throw new TestUsageError(`--godot names ${path}, which is not an engine executable.`);
57201
+ }
57202
+ return path;
57203
+ }
57204
+ function coresNamed(value) {
57205
+ const cores = whole(value, "--cores");
57206
+ const most = Math.min(availableParallelism(), 64);
57207
+ if (cores !== null && cores > most) {
57208
+ throw new TestUsageError(`--cores ${String(cores)} is more than the ${String(most)} processors a run can be held to here.`);
57209
+ }
57210
+ return cores;
57211
+ }
57212
+ function versionNamed(value) {
57213
+ if (value === undefined) {
57214
+ return null;
57215
+ }
57216
+ const version = value.replace(/^v/, "");
57217
+ if (!/^\d+\.\d+(?:\.\d+)?(?:\.[\w-]+)*$/.test(version)) {
57218
+ throw new TestUsageError(`--godot-version takes a version such as 4.7.2 or 4.7.2.stable, not ${value}.`);
57219
+ }
57220
+ return version;
57221
+ }
57078
57222
  function seedOf(value) {
57079
57223
  const seed = Number(value);
57080
57224
  if (!Number.isInteger(seed) || seed < 0) {
@@ -57102,7 +57246,7 @@ function homeOf(projectPath) {
57102
57246
  return mainCheckoutOf(projectPath) ?? projectPath;
57103
57247
  }
57104
57248
  function selectedSuites(scripts, options) {
57105
- const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync19(join22(options.projectPath, root))).map((root) => `res://${root}`);
57249
+ const roots = options.roots.length > 0 ? options.roots : TEST_DIRECTORIES.filter((root) => existsSync19(join23(options.projectPath, root))).map((root) => `res://${root}`);
57106
57250
  if (options.selected.length === 0) {
57107
57251
  const found = scripts.suitesUnder(roots);
57108
57252
  if (found.length === 0) {
@@ -57114,7 +57258,7 @@ function selectedSuites(scripts, options) {
57114
57258
  for (const entry of options.selected) {
57115
57259
  const caseAt = /^(res:\/\/.*\.gd):(\w+)$/.exec(entry);
57116
57260
  const path = caseAt?.[1] ?? entry;
57117
- const directory = join22(options.projectPath, path.slice(6));
57261
+ const directory = join23(options.projectPath, path.slice(6));
57118
57262
  const suites = existsSync19(directory) && statSync13(directory).isDirectory() ? scripts.suitesUnder([path.replace(/\/$/, "")]) : [scripts.suite(path)].filter((suite) => suite !== null);
57119
57263
  if (suites.length === 0) {
57120
57264
  throw new TestUsageError(existsSync19(directory) ? `${path} holds no gdUnit4 suite: nothing there extends GdUnitTestSuite and declares a test_ function.` : `Nothing at ${path} in the project at ${options.projectPath}.`);
@@ -57136,7 +57280,7 @@ function selectedSuites(scripts, options) {
57136
57280
  cases: suite.cases.filter((c) => cases.has(c))
57137
57281
  }));
57138
57282
  }
57139
- function roomFor(most, processors, available, said = "of memory free") {
57283
+ function roomFor(most, processors, available, said = "of memory free", processorsSaid = `the machine has ${plural(processors, "processor")}`) {
57140
57284
  const byMemory = Math.floor((available - MEMORY_KEPT) / MEMORY_PER_ENGINE);
57141
57285
  const engines = Math.max(1, Math.min(most, processors, byMemory));
57142
57286
  if (engines >= most) {
@@ -57145,7 +57289,7 @@ function roomFor(most, processors, available, said = "of memory free") {
57145
57289
  const gigabytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GB`;
57146
57290
  return {
57147
57291
  engines,
57148
- held: byMemory < processors ? `the machine has ${gigabytes(Math.max(0, available))} ${said}, ${gigabytes(MEMORY_KEPT)} of it kept for everything else and ${gigabytes(MEMORY_PER_ENGINE)} taken for each engine` : `the machine has ${plural(processors, "processor")}`
57292
+ held: byMemory < processors ? `the machine has ${gigabytes(Math.max(0, available))} ${said}, ${gigabytes(MEMORY_KEPT)} of it kept for everything else and ${gigabytes(MEMORY_PER_ENGINE)} taken for each engine` : processorsSaid
57149
57293
  };
57150
57294
  }
57151
57295
  async function memoryAvailable() {
@@ -57207,6 +57351,9 @@ function ordered(units, order) {
57207
57351
  }
57208
57352
  return sorted;
57209
57353
  }
57354
+ function heldToCores(cores, command, args, platform = process12.platform) {
57355
+ return cores === null || platform !== "linux" ? { command, args: [...args] } : { command: "taskset", args: ["-c", `0-${String(cores - 1)}`, command, ...args] };
57356
+ }
57210
57357
  async function runTestCommand(options, godotPath) {
57211
57358
  const unusable = gdUnit4Unusable(options.projectPath);
57212
57359
  if (unusable !== null) {
@@ -57215,17 +57362,17 @@ async function runTestCommand(options, godotPath) {
57215
57362
  const startedAt = Date.now();
57216
57363
  const scripts = new ProjectScripts(options.projectPath);
57217
57364
  const home = homeOf(options.projectPath);
57218
- const state = join22(home, ".godot", "gdharness");
57219
- const timesFile = join22(state, "suite-times.json");
57220
- const passedFile = join22(state, "suite-passed.json");
57365
+ const state = join23(home, ".godot", "gdharness");
57366
+ const timesFile = join23(state, "suite-times.json");
57367
+ const passedFile = join23(state, "suite-passed.json");
57221
57368
  let suites = selectedSuites(scripts, options);
57222
57369
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
57223
57370
  const declared = new Map(wholeSuites.map((suite) => [suite.path, suite.cases]));
57224
- const testRuns = join22(options.projectPath, ".godot", "gdharness", "test-runs");
57371
+ const testRuns = join23(options.projectPath, ".godot", "gdharness", "test-runs");
57225
57372
  sweepRunDirectories(testRuns);
57226
- const runDirectory = join22(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
57227
- mkdirSync8(runDirectory, { recursive: true });
57228
- const slots = new EngineSlots(options.slotsDirectory ?? join22(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
57373
+ const runDirectory = join23(testRuns, `${String(startedAt)}-${String(process12.pid)}`);
57374
+ mkdirSync9(runDirectory, { recursive: true });
57375
+ const slots = new EngineSlots(options.slotsDirectory ?? join23(home, ".godot", "gate", "slots"), options.slotCount ?? Math.min(MOST_ENGINES, availableParallelism()), { pid: process12.pid, started: await ownStart() });
57229
57376
  const running = new Set;
57230
57377
  const interrupted = () => {
57231
57378
  for (const end of running) {
@@ -57242,8 +57389,11 @@ async function runTestCommand(options, godotPath) {
57242
57389
  runDirectory,
57243
57390
  slots,
57244
57391
  silenceFloorSeconds: options.silenceSeconds,
57392
+ deadlineSeconds: options.deadlineSeconds,
57245
57393
  running,
57246
- selfTyped: selfTypedExports(scripts.sources)
57394
+ selfTyped: selfTypedExports(scripts.sources),
57395
+ godotVersion: options.godotVersion,
57396
+ cores: options.cores
57247
57397
  };
57248
57398
  try {
57249
57399
  if (options.importFirst) {
@@ -57282,7 +57432,10 @@ ${existsSync19(path) ? readFileSync20(path, "latin1") : "missing"}`)
57282
57432
  if (memory.unread !== undefined) {
57283
57433
  console.log(`Windows did not say how much memory it can still commit (${memory.unread}), so engines are sized by the physical memory free.`);
57284
57434
  }
57285
- const room = roomFor(options.engines, availableParallelism(), memory.bytes, memory.said);
57435
+ const room = options.cores === null ? roomFor(options.engines, availableParallelism(), memory.bytes, memory.said) : roomFor(options.engines, options.cores, memory.bytes, memory.said, `--cores holds it to ${plural(options.cores, "processor")}`);
57436
+ if (options.cores !== null && process12.platform === "darwin") {
57437
+ console.log("macOS offers no way to hold a process to some processors, so --cores holds only the number of engines here.");
57438
+ }
57286
57439
  const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
57287
57440
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
57288
57441
  const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(units, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
@@ -57378,19 +57531,38 @@ function sweepRunDirectories(testRuns) {
57378
57531
  for (const entry of readdirSync12(testRuns)) {
57379
57532
  const pid = Number(/^\d+-(\d+)$/.exec(entry)?.[1]);
57380
57533
  if (!Number.isInteger(pid) || !alive(pid)) {
57381
- rmSync10(join22(testRuns, entry), { recursive: true, force: true });
57534
+ rmSync10(join23(testRuns, entry), { recursive: true, force: true });
57382
57535
  }
57383
57536
  }
57384
57537
  }
57385
57538
  async function importProject(launch) {
57386
- const directory = join22(launch.runDirectory, "import");
57387
- mkdirSync8(join22(directory, "user"), { recursive: true });
57388
- const log = join22(directory, "import.log");
57539
+ const seeded = seedExtensionList(launch.projectPath);
57540
+ if (seeded !== null && seeded.length > 0) {
57541
+ console.log(`Wrote .godot/extension_list.cfg naming ${named(seeded)}, so the import loads them.`);
57542
+ }
57543
+ const cached = cachedClasses(launch.projectPath);
57544
+ if ((seeded === null || seeded.length === 0) && cached !== null && staleAgainst(cached, launch.projectPath).length === 0 && staleImports(launch.projectPath).length === 0) {
57545
+ console.log("The import was skipped: every imported file and the class cache are current.");
57546
+ return null;
57547
+ }
57548
+ const directory = join23(launch.runDirectory, "import");
57549
+ mkdirSync9(join23(directory, "user"), { recursive: true });
57550
+ const log = join23(directory, "import.log");
57389
57551
  const lock = await launch.slots.take();
57390
57552
  try {
57391
- const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, false, launch.silenceFloorSeconds);
57553
+ freeLibraryCopies(launch.projectPath);
57554
+ const ended = await watched(launch, ["--headless", "--audio-driver", "Dummy", "--path", launch.projectPath, "--import"], directory, log, launch.silenceFloorSeconds);
57392
57555
  if (ended.ended !== null || ended.code !== 0) {
57393
- return `${ended.ended ?? `the engine exited ${String(ended.code)}`}; everything it printed is in ${log}`;
57556
+ return `${ended.ended ?? `the engine exited ${exitNamed(ended.code ?? 0)}`}; everything it printed is in ${log}`;
57557
+ }
57558
+ const printed = readRun(readFileSync20(log, "utf8"));
57559
+ const wrongEngine = launch.godotVersion === null ? null : versionFault(printed.engineVersion, launch.godotVersion);
57560
+ if (wrongEngine !== null) {
57561
+ return `${wrongEngine}; everything it printed is in ${log}`;
57562
+ }
57563
+ const errors = printed.errors.filter((error) => !error.warning);
57564
+ if (errors.length > 0) {
57565
+ return `it printed ${errors.length === 1 ? "an engine error" : `${String(errors.length)} engine errors`}, the first ${JSON.stringify(errors[0]?.text ?? "")}; everything it printed is in ${log}`;
57394
57566
  }
57395
57567
  return null;
57396
57568
  } finally {
@@ -57410,19 +57582,19 @@ async function runLane(launch, lane, units) {
57410
57582
  }
57411
57583
  }
57412
57584
  async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57413
- const directory = join22(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
57414
- mkdirSync8(join22(directory, "user"), { recursive: true });
57415
- const suitesFile = join22(directory, "suites.txt");
57416
- const skippingFile = join22(directory, "skipping.txt");
57417
- const rowsFile = join22(directory, "rows.txt");
57418
- const expectedFile = join22(directory, "expected.json");
57419
- writeFileSync10(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
57585
+ const directory = join23(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
57586
+ mkdirSync9(join23(directory, "user"), { recursive: true });
57587
+ const suitesFile = join23(directory, "suites.txt");
57588
+ const skippingFile = join23(directory, "skipping.txt");
57589
+ const rowsFile = join23(directory, "rows.txt");
57590
+ const expectedFile = join23(directory, "expected.json");
57591
+ writeFileSync11(suitesFile, `${plan.suites.map((suite) => suite.path).join(`
57420
57592
  `)}
57421
57593
  `);
57422
- writeFileSync10(skippingFile, `${plan.skipping.join(`
57594
+ writeFileSync11(skippingFile, `${plan.skipping.join(`
57423
57595
  `)}
57424
57596
  `);
57425
- writeFileSync10(rowsFile, `${plan.suites.flatMap((suite) => Object.entries(suite.rows ?? {}).map(([func, share]) => [
57597
+ writeFileSync11(rowsFile, `${plan.suites.flatMap((suite) => Object.entries(suite.rows ?? {}).map(([func, share]) => [
57426
57598
  `${suite.path}:${func}`,
57427
57599
  ..."only" in share ? ["only", ...share.only] : ["besides", ...share.besides]
57428
57600
  ].join("\t"))).join(`
@@ -57441,7 +57613,7 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57441
57613
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
57442
57614
  "-c",
57443
57615
  "-rd",
57444
- `res://${relative3(launch.projectPath, join22(directory, "reports")).split(sep2).join("/")}`,
57616
+ `res://${relative3(launch.projectPath, join23(directory, "reports")).split(sep2).join("/")}`,
57445
57617
  "-rc",
57446
57618
  "1",
57447
57619
  "--",
@@ -57460,12 +57632,12 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57460
57632
  const held = new Set(plan.suites.map((suite) => suite.path));
57461
57633
  const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
57462
57634
  const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
57463
- const log = join22(directory, "engine.log");
57635
+ const log = join23(directory, "engine.log");
57464
57636
  const lock = slot ?? await launch.slots.take();
57465
57637
  const began = Date.now();
57466
57638
  let end;
57467
57639
  try {
57468
- end = await watched(launch, args, directory, log, plan.windowed, silence);
57640
+ end = await watched(launch, args, directory, log, silence);
57469
57641
  } finally {
57470
57642
  if (slot === null) {
57471
57643
  launch.slots.release(lock);
@@ -57475,7 +57647,8 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57475
57647
  const assignment = {
57476
57648
  suites: plan.suites,
57477
57649
  expected: readExpected(expectedFile),
57478
- selfTyped: launch.selfTyped
57650
+ selfTyped: launch.selfTyped,
57651
+ godotVersion: launch.godotVersion
57479
57652
  };
57480
57653
  return {
57481
57654
  index,
@@ -57483,29 +57656,34 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
57483
57656
  printed,
57484
57657
  verdict: judgeEngine(printed, assignment, end),
57485
57658
  log,
57486
- seconds: (Date.now() - began) / 1000
57659
+ seconds: (Date.now() - began) / 1000,
57660
+ again: again !== ""
57487
57661
  };
57488
57662
  }
57489
- async function watched(launch, args, directory, log, windowed, silenceSeconds) {
57490
- const user = join22(directory, "user");
57663
+ async function watched(launch, args, directory, log, silenceSeconds) {
57664
+ const user = join23(directory, "user");
57491
57665
  const env = { ...process12.env, APPDATA: user, XDG_DATA_HOME: user };
57492
57666
  const output = openSync3(log, "w");
57493
57667
  let child;
57494
57668
  let stop;
57495
57669
  let helper = null;
57496
57670
  try {
57497
- if (process12.platform === "win32" && (windowed || !crashesReported())) {
57498
- const where = windowed ? "on a desktop of its own" : "through the desktop helper";
57671
+ if (process12.platform === "win32") {
57672
+ const where = "on a desktop of its own";
57499
57673
  const compiled = await ensureHelperCompiled();
57500
57674
  if (compiled !== null) {
57501
57675
  return { code: null, signal: null, ended: `the engine could not be started ${where}: ${compiled}` };
57502
57676
  }
57503
- const stopFile = join22(directory, "stop");
57504
- const error = join22(directory, "error");
57505
- helper = { error, how: where, pid: join22(directory, "pid"), step: join22(directory, "step") };
57506
- const how = windowed ? { desktop: HIDDEN_DESKTOP, directory: launch.projectPath } : { directory: launch.projectPath };
57677
+ const stopFile = join23(directory, "stop");
57678
+ const error = join23(directory, "error");
57679
+ helper = { error, how: where, pid: join23(directory, "pid"), step: join23(directory, "step") };
57680
+ const how = {
57681
+ desktop: HIDDEN_DESKTOP,
57682
+ directory: launch.projectPath,
57683
+ ...launch.cores === null ? {} : { affinity: (1n << BigInt(launch.cores)) - 1n }
57684
+ };
57507
57685
  const wrapper = throughHelper(how, launch.godotPath, args, {
57508
- spec: join22(directory, "spec"),
57686
+ spec: join23(directory, "spec"),
57509
57687
  pid: helper.pid,
57510
57688
  error,
57511
57689
  stop: stopFile,
@@ -57513,18 +57691,19 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
57513
57691
  });
57514
57692
  child = spawnHidden(wrapper.command, wrapper.args, {
57515
57693
  stdio: ["ignore", output, output],
57516
- env: windowed ? { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP } : env,
57694
+ env: { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP },
57517
57695
  cwd: directory
57518
57696
  });
57519
57697
  stop = () => {
57520
- writeFileSync10(stopFile, "");
57698
+ writeFileSync11(stopFile, "");
57521
57699
  };
57522
57700
  } else {
57523
- child = spawnHidden(launch.godotPath, args, {
57701
+ const held = heldToCores(launch.cores, launch.godotPath, args);
57702
+ child = spawnHidden(held.command, held.args, {
57524
57703
  stdio: ["ignore", output, output],
57525
57704
  env,
57526
57705
  cwd: launch.projectPath,
57527
- detached: process12.platform !== "win32"
57706
+ detached: true
57528
57707
  });
57529
57708
  stop = () => {
57530
57709
  endTree(child);
@@ -57551,10 +57730,17 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
57551
57730
  const reading = openSync3(log, "r");
57552
57731
  try {
57553
57732
  let size = 0;
57554
- let lastGrowth = Date.now();
57733
+ const started = Date.now();
57734
+ let lastGrowth = started;
57555
57735
  let text = "";
57556
57736
  while (!finished.now) {
57557
57737
  await delay3(LOOK_MS);
57738
+ const deadline = launch.deadlineSeconds;
57739
+ if (ended === null && deadline !== null && Date.now() - started > deadline * 1000) {
57740
+ const stoppedIn = readRun(text).stoppedIn;
57741
+ ended = `the engine ran past the ${String(deadline)} s deadline${stoppedIn === null ? "" : ` while running ${stoppedIn}`}, so it was ended`;
57742
+ stop();
57743
+ }
57558
57744
  const now = fstatSync(reading).size;
57559
57745
  if (now > size) {
57560
57746
  const chunk = Buffer.alloc(now - size);
@@ -57578,11 +57764,12 @@ async function watched(launch, args, directory, log, windowed, silenceSeconds) {
57578
57764
  launch.running.delete(stop);
57579
57765
  }
57580
57766
  const { code, signal } = await exited;
57767
+ const freeBytes = code !== null && code >>> 0 >= WINDOWS_EXCEPTION ? freemem() : undefined;
57581
57768
  const failed = helper === null ? null : helperFailure(helper.error);
57582
57769
  if (failed !== null && helper !== null) {
57583
57770
  ended ??= `the engine could not be started ${helper.how}: ${failed}`;
57584
57771
  }
57585
- return { code, signal, ended };
57772
+ return { code, signal, ended, ...freeBytes === undefined ? {} : { freeBytes } };
57586
57773
  }
57587
57774
  function endTree(child) {
57588
57775
  if (child.pid === undefined) {
@@ -57606,11 +57793,22 @@ function report(ran, across, startedAt, runDirectory, printOutput) {
57606
57793
  }
57607
57794
  const failed = ran.filter((engine) => !engine.verdict.passed);
57608
57795
  for (const engine of failed) {
57796
+ const heading = `--- engine ${String(engine.index)}: ${plural(engine.plan.suites.length, "suite")}, ${engine.seconds.toFixed(1)} s${engine.again ? ", run again alone after an engine ran out of memory, and failed again" : ""}`;
57609
57797
  console.log("");
57610
- console.log(`--- engine ${String(engine.index)}: ${plural(engine.plan.suites.length, "suite")}, ${engine.seconds.toFixed(1)} s`);
57798
+ console.log(heading);
57611
57799
  for (const fault of engine.verdict.faults) {
57612
57800
  console.log(` ${fault}`);
57613
57801
  }
57802
+ if (existsSync19(engine.log)) {
57803
+ const verdict = [
57804
+ `gdharness: ${heading.slice(4)}`,
57805
+ ...engine.verdict.faults.map((fault) => ` ${fault}`)
57806
+ ];
57807
+ writeFileSync11(engine.log, `${verdict.join(`
57808
+ `)}
57809
+ ---
57810
+ ${readFileSync20(engine.log, "utf8")}`);
57811
+ }
57614
57812
  if (engine.verdict.shown.length > 0) {
57615
57813
  console.log("");
57616
57814
  for (const line of engine.verdict.shown) {
@@ -57630,6 +57828,14 @@ function report(ran, across, startedAt, runDirectory, printOutput) {
57630
57828
  const suites = new Set(ran.flatMap((engine) => engine.plan.suites.map((suite) => suite.path))).size;
57631
57829
  const longest = Math.max(0, ...ran.map((engine) => engine.seconds));
57632
57830
  const took = (Date.now() - startedAt) / 1000;
57831
+ if (ran.length > 1) {
57832
+ console.log("");
57833
+ console.log(`The engines took ${ran.map((engine) => `${engine.seconds.toFixed(1)} s`).join(", ")}, dealt ${ran.map((engine) => `${engine.plan.cost.toFixed(1)} s`).join(", ")}.`);
57834
+ }
57835
+ const slowest = ran.flatMap((engine) => [...engine.printed.suites.values()].flatMap((suite) => suite.closed === null ? [] : [{ path: suite.path, seconds: suite.closed.seconds }])).sort((a, b) => b.seconds - a.seconds).slice(0, SLOWEST_NAMED);
57836
+ if (slowest.length > 1) {
57837
+ console.log(`The slowest suites: ${slowest.map((suite) => `${suite.path} ${suite.seconds.toFixed(1)} s`).join(", ")}.`);
57838
+ }
57633
57839
  const totals = `${plural(cases, "case")} in ${plural(suites, "suite")} across ${plural(ran.length, "engine")}, ${took.toFixed(1)} s; the longest engine took ${longest.toFixed(1)} s.`;
57634
57840
  console.log("");
57635
57841
  if (failed.length === 0 && across.length === 0) {
@@ -57652,7 +57858,7 @@ class UsageError extends Refusal {
57652
57858
  function projectArgument(at) {
57653
57859
  const named = args.slice(at).find((value) => !value.startsWith("--"));
57654
57860
  const projectPath = resolve10(named ?? ".");
57655
- if (!existsSync31(join39(projectPath, "project.godot"))) {
57861
+ if (!existsSync31(join40(projectPath, "project.godot"))) {
57656
57862
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
57657
57863
  }
57658
57864
  return projectPath;
@@ -57697,7 +57903,7 @@ async function registerRuntime(godot, projectPath) {
57697
57903
  return;
57698
57904
  }
57699
57905
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
57700
- if (!existsSync31(join39(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
57906
+ if (!existsSync31(join40(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
57701
57907
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
57702
57908
  }
57703
57909
  }
@@ -58069,7 +58275,8 @@ Usage:
58069
58275
  an export if it is left on
58070
58276
  gdharness classes [project] Rebuild .godot/global_script_class_cache.cfg from disk
58071
58277
  gdharness test [project] [suite ...] [--engines N] [--slots DIR] [--slot-count N]
58072
- [--silence S] [--tests DIR] [--changed-only] [--reach-also PATH] [--import]
58278
+ [--silence S] [--deadline S] [--tests DIR] [--changed-only] [--reach-also PATH]
58279
+ [--import] [--godot PATH] [--godot-version V] [--cores N]
58073
58280
  [--order reversed|SEED] [--print-output] [--runner]
58074
58281
  Run the project's gdUnit4 suites over several engines and
58075
58282
  judge every line they print: exit 1 on a failing case, an
@@ -58103,7 +58310,7 @@ async function test() {
58103
58310
  return;
58104
58311
  }
58105
58312
  const options = testOptionsOf(rest, process.cwd());
58106
- const { godotPath } = await engine(options.projectPath);
58313
+ const godotPath = options.godotPath ?? (await engine(options.projectPath)).godotPath;
58107
58314
  process.exitCode = await runTestCommand(options, godotPath);
58108
58315
  }
58109
58316
  async function main() {