gdharness 1.3.38 → 1.3.40

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
@@ -852,7 +852,18 @@ public static class GdharnessDesktop {
852
852
  }
853
853
 
854
854
  [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
855
- static extern IntPtr CreateDesktopW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes);
855
+ static extern IntPtr CreateDesktopExW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes, uint heapKilobytes, IntPtr reserved);
856
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
857
+ static extern IntPtr CreateWindowStationW(string name, uint flags, uint access, IntPtr attributes);
858
+ [DllImport("user32.dll", SetLastError = true)] static extern IntPtr GetProcessWindowStation();
859
+ [DllImport("user32.dll", SetLastError = true)] static extern bool SetProcessWindowStation(IntPtr station);
860
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
861
+ static extern bool GetUserObjectInformationW(IntPtr handle, int index, StringBuilder into, int size, out int needed);
862
+ const uint WindowStationAllAccess = 0x37F;
863
+ const int ObjectName = 2;
864
+ // What Windows gives an interactive desktop. A non-interactive station's desktops get a small
865
+ // fraction of that by default, and one desktop here holds every engine a run starts.
866
+ const uint DesktopHeapKilobytes = 20480;
856
867
 
857
868
  [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
858
869
  static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out Started started);
@@ -943,12 +954,34 @@ public static class GdharnessDesktop {
943
954
  StartupInfo startup = new StartupInfo();
944
955
  startup.cb = Marshal.SizeOf(typeof(StartupInfo));
945
956
  if (desktop.Length > 0) {
946
- Step(stepFile, "creating the " + desktop + " desktop");
947
- IntPtr held = CreateDesktopW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero);
957
+ Step(stepFile, "opening the session's own window station");
958
+ IntPtr station = CreateWindowStationW(null, 0, WindowStationAllAccess, IntPtr.Zero);
959
+ if (station == IntPtr.Zero) {
960
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateWindowStationW");
961
+ }
962
+ StringBuilder stationName = new StringBuilder(256);
963
+ int needed;
964
+ if (!GetUserObjectInformationW(station, ObjectName, stationName, stationName.Capacity * 2, out needed)) {
965
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "GetUserObjectInformationW");
966
+ }
967
+ // A desktop is made in the calling process's station, so this one is the caller's while it is.
968
+ IntPtr own = GetProcessWindowStation();
969
+ if (!SetProcessWindowStation(station)) {
970
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "SetProcessWindowStation");
971
+ }
972
+ IntPtr held;
973
+ int made;
974
+ try {
975
+ Step(stepFile, "creating the " + desktop + " desktop");
976
+ held = CreateDesktopExW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero, DesktopHeapKilobytes, IntPtr.Zero);
977
+ made = Marshal.GetLastWin32Error();
978
+ } finally {
979
+ SetProcessWindowStation(own);
980
+ }
948
981
  if (held == IntPtr.Zero) {
949
- throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateDesktopW " + desktop);
982
+ throw new Win32Exception(made, "CreateDesktopExW " + stationName + "\\\\" + desktop);
950
983
  }
951
- startup.desktop = desktop;
984
+ startup.desktop = stationName + "\\\\" + desktop;
952
985
  }
953
986
  startup.flags = UseStdHandles;
954
987
  if (noActivate) {
@@ -11334,7 +11367,8 @@ import {
11334
11367
  readSync,
11335
11368
  rmSync as rmSync4,
11336
11369
  statSync as statSync5,
11337
- writeFileSync as writeFileSync4
11370
+ writeFileSync as writeFileSync4,
11371
+ writeSync as writeSync2
11338
11372
  } from "node:fs";
11339
11373
  import { availableParallelism, freemem, totalmem } from "node:os";
11340
11374
  import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
@@ -11489,6 +11523,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11489
11523
  const output = openSync2(log, "w");
11490
11524
  let child;
11491
11525
  let stop;
11526
+ let piped = null;
11492
11527
  let helper = null;
11493
11528
  try {
11494
11529
  if (process6.platform === "win32") {
@@ -11512,11 +11547,13 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11512
11547
  stop: stopFile,
11513
11548
  step: helper.step
11514
11549
  });
11515
- child = spawnHidden(wrapper.command, wrapper.args, {
11516
- stdio: ["ignore", output, output],
11550
+ const helped = spawnHidden(wrapper.command, wrapper.args, {
11551
+ stdio: ["ignore", "pipe", "pipe"],
11517
11552
  env: { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP },
11518
11553
  cwd: directory
11519
11554
  });
11555
+ piped = [helped.stdout, helped.stderr];
11556
+ child = helped;
11520
11557
  stop = () => {
11521
11558
  writeFileSync4(stopFile, "");
11522
11559
  };
@@ -11533,7 +11570,9 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11533
11570
  };
11534
11571
  }
11535
11572
  } finally {
11536
- closeSync2(output);
11573
+ if (piped === null) {
11574
+ closeSync2(output);
11575
+ }
11537
11576
  }
11538
11577
  launch.running.add(stop);
11539
11578
  let ended = null;
@@ -11546,6 +11585,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11546
11585
  });
11547
11586
  })
11548
11587
  ]);
11588
+ const copied = piped === null ? null : copiedInto(output, piped, exited);
11549
11589
  const finished = { now: false };
11550
11590
  exited.then(() => {
11551
11591
  finished.now = true;
@@ -11598,12 +11638,42 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
11598
11638
  }
11599
11639
  const { code, signal } = await exited;
11600
11640
  const freeBytes = code !== null && code >>> 0 >= WINDOWS_EXCEPTION ? freemem() : undefined;
11641
+ if (copied !== null) {
11642
+ await copied;
11643
+ closeSync2(output);
11644
+ }
11601
11645
  const failed = helper === null ? null : helperFailure(helper.error);
11602
11646
  if (failed !== null && helper !== null) {
11603
11647
  ended ??= `the engine could not be started ${helper.how}: ${failed}`;
11604
11648
  }
11605
11649
  return { code, signal, ended, ...freeBytes === undefined ? {} : { freeBytes } };
11606
11650
  }
11651
+ function copiedInto(output, streams, exited) {
11652
+ let writing = true;
11653
+ const ended = streams.map((stream) => new Promise((settle) => {
11654
+ stream.on("data", (chunk) => {
11655
+ if (!writing) {
11656
+ return;
11657
+ }
11658
+ try {
11659
+ writeSync2(output, chunk);
11660
+ } catch {
11661
+ writing = false;
11662
+ }
11663
+ });
11664
+ stream.once("close", settle);
11665
+ }));
11666
+ const all = Promise.all(ended).then(() => {
11667
+ return;
11668
+ });
11669
+ const drained = exited.then(() => delay2(DRAIN_MS, undefined, { ref: false }));
11670
+ return Promise.race([all, drained]).then(() => {
11671
+ writing = false;
11672
+ for (const stream of streams) {
11673
+ stream.destroy();
11674
+ }
11675
+ });
11676
+ }
11607
11677
  function endTree(child) {
11608
11678
  if (child.pid === undefined) {
11609
11679
  return;
@@ -11616,7 +11686,7 @@ function endTree(child) {
11616
11686
  process6.kill(-child.pid, "SIGKILL");
11617
11687
  } catch {}
11618
11688
  }
11619
- var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5;
11689
+ var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5, DRAIN_MS = 5000;
11620
11690
  var init_engine_launch = __esm(() => {
11621
11691
  init_alive();
11622
11692
  init_desktop();
@@ -11744,7 +11814,7 @@ import {
11744
11814
  rmSync as rmSync6,
11745
11815
  statSync as statSync6,
11746
11816
  watch,
11747
- writeSync as writeSync2
11817
+ writeSync as writeSync3
11748
11818
  } from "node:fs";
11749
11819
  import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
11750
11820
  import { join as join8, resolve as resolve4 } from "node:path";
@@ -11970,7 +12040,7 @@ class EngineSlots {
11970
12040
  return false;
11971
12041
  }
11972
12042
  try {
11973
- writeSync2(made, JSON.stringify(this.holder));
12043
+ writeSync3(made, JSON.stringify(this.holder));
11974
12044
  } finally {
11975
12045
  closeSync3(made);
11976
12046
  }
@@ -12084,7 +12154,7 @@ async function inASlot(body) {
12084
12154
  held.release();
12085
12155
  }
12086
12156
  }
12087
- var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 5, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", waitsMarked = 0, ours, saidTo, takenBy;
12157
+ var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", waitsMarked = 0, ours, saidTo, takenBy;
12088
12158
  var init_engine_slots = __esm(() => {
12089
12159
  init_alive();
12090
12160
  init_call_signal();
@@ -17364,7 +17434,7 @@ var init_tool_definitions = __esm(() => {
17364
17434
  name: "script_diagnostics",
17365
17435
  title: "Script diagnostics",
17366
17436
  effect: "read",
17367
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine rather than language server: every error that stops a script compiling, up to its first syntax error, with its line and no column (a call to a method another class lacks is not one, since GDScript checks those only on self: the object may be a subclass that has it, so it fails at run time instead), dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and warnings null, since the engine reports warnings only to an editor. Of several scripts, a language server that stops answering partway leaves the rest to the engine, and checkedBy then says language server, then engine, with checkedByLanguageServer counting the scripts it answered. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
17437
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine rather than language server: every error that stops a script compiling, up to its first syntax error, with its line and no column (a call to a method another class lacks is not one, since GDScript checks those only on self: the object may be a subclass that has it, so it fails at run time instead), dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and the warnings at the levels the project sets, which a second engine reports by raising each warning set to warn as an error for that run only, each at its line with no column, warnings being null when that engine could not answer. Of several scripts, a language server that stops answering partway leaves the rest to the engine, and checkedBy then says language server, then engine, with checkedByLanguageServer counting the scripts it answered. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
17368
17438
  parameters: {
17369
17439
  projectPath: PROJECT_PATH,
17370
17440
  scriptPath: {
@@ -17567,7 +17637,7 @@ var init_tool_definitions = __esm(() => {
17567
17637
  visible: {
17568
17638
  type: "boolean",
17569
17639
  ops: ["start"],
17570
- description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
17640
+ description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard or the pointer. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
17571
17641
  },
17572
17642
  silent: {
17573
17643
  type: "boolean",
@@ -18665,6 +18735,7 @@ class ProjectScripts {
18665
18735
  suite(path) {
18666
18736
  const cases = new Set;
18667
18737
  let said;
18738
+ let together;
18668
18739
  let guessed = false;
18669
18740
  const seen = new Set;
18670
18741
  let at = path;
@@ -18679,13 +18750,21 @@ class ProjectScripts {
18679
18750
  }
18680
18751
  const declared = NEEDS_WINDOW.exec(read.code)?.[1];
18681
18752
  said ??= declared === undefined ? undefined : declared === "true";
18753
+ const kept = TOGETHER.exec(read.code)?.[1];
18754
+ together ??= kept === undefined ? undefined : kept === "true";
18682
18755
  guessed ||= this.guessWindow && SCENE_RUNNER.test(read.code);
18683
18756
  const base = this.base(at);
18684
18757
  if (base === null) {
18685
18758
  return null;
18686
18759
  }
18687
18760
  if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
18688
- return cases.size > 0 ? { path, cases: [...cases].sort(), windowed: said ?? guessed, headless: said === false } : null;
18761
+ return cases.size > 0 ? {
18762
+ path,
18763
+ cases: [...cases].sort(),
18764
+ windowed: said ?? guessed,
18765
+ headless: said === false,
18766
+ together: together === true
18767
+ } : null;
18689
18768
  }
18690
18769
  if ("name" in base) {
18691
18770
  return null;
@@ -18808,6 +18887,10 @@ function rowsAfter(before, measured, cutShort) {
18808
18887
  function round(seconds) {
18809
18888
  return Math.round(seconds * 1000) / 1000;
18810
18889
  }
18890
+ function splitPastFor(suites, times, engines) {
18891
+ const whole = unitsOf(suites, times, Number.POSITIVE_INFINITY).reduce((sum, unit) => sum + unit.cost, 0);
18892
+ return Math.max(SPLIT_PAST_SECONDS, whole / Math.max(1, engines));
18893
+ }
18811
18894
  function median(values) {
18812
18895
  if (values.length === 0) {
18813
18896
  return;
@@ -18834,7 +18917,7 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
18834
18917
  windowed: suite.windowed,
18835
18918
  headless: suite.headless
18836
18919
  };
18837
- if (suite.windowed || whole <= splitPast) {
18920
+ if (suite.windowed || suite.together || whole <= splitPast) {
18838
18921
  return [wholly];
18839
18922
  }
18840
18923
  const units = suite.cases.flatMap((func) => {
@@ -19003,13 +19086,14 @@ function viewportOf(projectPath) {
19003
19086
  const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
19004
19087
  return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
19005
19088
  }
19006
- var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
19089
+ var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TOGETHER, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
19007
19090
  var init_suite_plan = __esm(() => {
19008
19091
  init_class_cache();
19009
19092
  init_gdscript_source();
19010
19093
  init_resources();
19011
19094
  init_whole_file();
19012
19095
  NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
19096
+ TOGETHER = /^const\s+GDHARNESS_TOGETHER\b[^=\n]*=\s*(true|false)\b/m;
19013
19097
  TEST_DIRECTORIES = ["tests", "test"];
19014
19098
  SCENE_RUNNER = /\bscene_runner\s*\(/;
19015
19099
  SHARE_FLOOR_SECONDS = 6 * ENGINE_START_SECONDS;
@@ -50416,9 +50500,13 @@ function engineChecked(why) {
50416
50500
  return {
50417
50501
  checkedBy: "engine",
50418
50502
  languageServer: why,
50419
- note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, but stops reading a script at its first syntax error, so an error after one shows once it is fixed. It reports no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both. Until then clean says only that every script compiles."
50503
+ note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, but stops reading a script at its first syntax error, so an error after one shows once it is fixed. A second engine reports the warnings, at the levels the project sets, by raising each warning set to warn as an error for that run only, since the engine tells a warning as one only to an editor; each comes at its line without a column, and warnings is null when that engine could not answer. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both with their columns."
50420
50504
  };
50421
50505
  }
50506
+ function asWarning(raised) {
50507
+ const bare = raised.endsWith(RAISED_WARNING) ? raised.slice(0, -RAISED_WARNING.length) : raised;
50508
+ return bare.startsWith(PARSE_ERROR2) ? bare.slice(PARSE_ERROR2.length) : bare;
50509
+ }
50422
50510
  function isDirectory(path) {
50423
50511
  try {
50424
50512
  return statSync20(path).isDirectory();
@@ -53206,6 +53294,7 @@ ${error.text}`;
53206
53294
  const nothingReported = [];
53207
53295
  let errors = 0;
53208
53296
  let warnings = 0;
53297
+ let engineWarned = 0;
53209
53298
  let unread = 0;
53210
53299
  let byEngine = null;
53211
53300
  let byServer = 0;
@@ -53224,6 +53313,7 @@ ${error.text}`;
53224
53313
  }
53225
53314
  for (const answer of checked.answers) {
53226
53315
  errors += Number(answer["errors"]);
53316
+ engineWarned = answer["warnings"] === null || engineWarned === null ? null : engineWarned + Number(answer["warnings"]);
53227
53317
  if (readArray(answer, "diagnostics")?.length === 0) {
53228
53318
  nothingReported.push(String(answer["scriptPath"]));
53229
53319
  } else {
@@ -53254,7 +53344,7 @@ ${error.text}`;
53254
53344
  clean: errors === 0 && unread === 0,
53255
53345
  scriptsChecked: scripts.value.length,
53256
53346
  errors,
53257
- warnings: byEngine === null ? warnings : null,
53347
+ warnings: engineWarned === null ? null : warnings + engineWarned,
53258
53348
  ...unread > 0 ? { scriptsNotRead: unread } : {},
53259
53349
  scripts: reported,
53260
53350
  nothingReported,
@@ -53289,6 +53379,20 @@ ${error.text}`;
53289
53379
  }
53290
53380
  return { ok: true, payload, extras: engineExtras({ messages }) };
53291
53381
  }
53382
+ async engineWarnings(projectPath, paths) {
53383
+ const ran = await this.operation("check_warnings", { paths: [...paths] }, projectPath);
53384
+ if (!ran.ok) {
53385
+ this.logDebug(`check_warnings did not answer: ${ran.message}`);
53386
+ return null;
53387
+ }
53388
+ return new Map((readArray(ran.payload, "scripts") ?? []).map((entry) => [
53389
+ String(asParams(entry)["path"]),
53390
+ (readArray(asParams(entry), "warnings") ?? []).map((warning) => ({
53391
+ line: Number(asParams(warning)["line"]),
53392
+ message: String(asParams(warning)["message"])
53393
+ }))
53394
+ ]));
53395
+ }
53292
53396
  async engineDiagnostics(projectPath, scripts, why) {
53293
53397
  const named = scripts.map((script) => {
53294
53398
  const contained = resolveWithinProject(projectPath, script);
@@ -53305,21 +53409,25 @@ ${error.text}`;
53305
53409
  asParams(entry)
53306
53410
  ]));
53307
53411
  const errorsOf = scripts.map((_, at) => readArray(failures.get(named[at] ?? "") ?? {}, "errors") ?? []);
53412
+ const warned = await this.engineWarnings(projectPath, named);
53308
53413
  const uncachedPer = uncachedFor(projectPath, errorsOf.map((errors) => errors.map((entry) => String(asParams(entry)["message"]))));
53309
53414
  return {
53310
53415
  answers: scripts.map((scriptPath, at) => {
53311
53416
  const failure = failures.get(named[at] ?? "");
53312
- const diagnostics = (errorsOf[at] ?? []).map((entry) => {
53417
+ const errors = (errorsOf[at] ?? []).map((entry) => {
53313
53418
  const { line, message } = asParams(entry);
53314
53419
  return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
53315
53420
  });
53421
+ const raised = new Set(errors.map(({ message, line }) => `${line ?? 0}:${String(message)}`));
53422
+ const warnings = warned === null ? [] : (warned.get(named[at] ?? "") ?? []).filter(({ line, message }) => !raised.has(`${line}:${message}`)).map(({ line, message }) => ({ severity: "warning", message: asWarning(message), line }));
53423
+ const diagnostics = [...errors, ...warnings].sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
53316
53424
  const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
53317
53425
  const uncached = uncachedPer[at] ?? [];
53318
53426
  return {
53319
53427
  scriptPath,
53320
- clean: diagnostics.length === 0,
53321
- errors: diagnostics.length,
53322
- warnings: null,
53428
+ clean: errors.length === 0,
53429
+ errors: errors.length,
53430
+ warnings: warned === null ? null : warnings.length,
53323
53431
  diagnostics,
53324
53432
  ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies },
53325
53433
  ...uncached.length === 0 ? {} : {
@@ -57669,7 +57777,7 @@ async function runGodotServer() {
57669
57777
  const server = new GodotServer;
57670
57778
  await server.run();
57671
57779
  }
57672
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
57780
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
57673
57781
  var init_server = __esm(() => {
57674
57782
  init_dist2();
57675
57783
  init_stdio();
@@ -58160,7 +58268,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
58160
58268
  These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
58161
58269
  window and refuses headless rather than handing back the last frame anything drew. On Windows a
58162
58270
  windowed run this server starts is on a desktop of its own, where it draws and can be captured but
58163
- never shows or takes the keyboard.
58271
+ never shows or takes the keyboard or the pointer.
58164
58272
 
58165
58273
  The autoload reads these settings and no others. \`${BIND_ADDRESS_SETTING}\`
58166
58274
  is \`127.0.0.1\` and should stay there: the command set includes \`call_method\`, \`set_property\`
@@ -59116,7 +59224,6 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59116
59224
  }
59117
59225
  }
59118
59226
  const times = readTimes(timesFile);
59119
- const units = unitsOf(suites, times);
59120
59227
  const memory = await memoryAvailable();
59121
59228
  if (memory.unread !== undefined) {
59122
59229
  console.log(`Windows did not say how much memory it can still commit (${memory.unread}), so engines are sized by the physical memory free.`);
@@ -59125,9 +59232,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59125
59232
  if (options.cores !== null && process14.platform === "darwin") {
59126
59233
  console.log("macOS offers no way to hold a process to some processors, so --cores holds only the number of engines here.");
59127
59234
  }
59235
+ const splitPast = splitPastFor(suites, times, room.engines);
59236
+ const units = unitsOf(suites, times, splitPast);
59237
+ const past = splitPast > SPLIT_PAST_SECONDS ? `an engine's share of this run (${String(Math.round(splitPast))} s)` : `${String(SPLIT_PAST_SECONDS)} s`;
59128
59238
  const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
59129
59239
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
59130
- 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)}`;
59240
+ const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
59241
+ const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(asked, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
59131
59242
  console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59132
59243
  const windowedAnyway = headlessInAWindow(plans, units);
59133
59244
  if (windowedAnyway.length > 0) {
@@ -59136,13 +59247,18 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59136
59247
  }
59137
59248
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
59138
59249
  if (plans.length > 1 && split.length > 0) {
59139
- console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(split)}.`);
59250
+ console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59140
59251
  }
59141
59252
  const byRows = [
59142
59253
  ...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
59143
59254
  ];
59144
59255
  if (plans.length > 1 && byRows.length > 0) {
59145
- console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(byRows)}.`);
59256
+ console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59257
+ }
59258
+ const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
59259
+ if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
59260
+ const one = keptWhole.length === 1;
59261
+ console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59146
59262
  }
59147
59263
  const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
59148
59264
  const taken = [];
@@ -125,15 +125,16 @@ static func no_animation(player: AnimationPlayer, called: String, animation_name
125
125
 
126
126
  static func _summary(anim: Animation) -> Dictionary:
127
127
  return {
128
- "length": anim.length,
128
+ "length": held_seconds(anim.length),
129
129
  "loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
130
- "step": anim.step,
130
+ "step": held_seconds(anim.step),
131
131
  }
132
132
 
133
133
 
134
- ## [param seconds], a key's time, as the shortest decimal holding the same 32-bit float. A scene keeps
135
- ## key times as 32-bit floats, so a key put at 0.7 read back as 0.699999988079071.
136
- static func key_time(seconds: float) -> float:
134
+ ## [param seconds], a key's time or an animation's length or step, as the shortest decimal holding
135
+ ## the same 32-bit float. An animation keeps all three as 32-bit floats, so a key put at 0.7 read
136
+ ## back as 0.699999988079071 and a step of 0.1 as 0.100000001490116.
137
+ static func held_seconds(seconds: float) -> float:
137
138
  var held: float = PackedFloat32Array([seconds])[0]
138
139
  for places: int in range(0, 10):
139
140
  var shorter: float = String.num(held, places).to_float()
@@ -159,7 +160,7 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
159
160
  var keyframes: Array[Dictionary] = []
160
161
  var methods: Dictionary = {}
161
162
  for key: int in anim.track_get_key_count(track):
162
- var keyframe: Dictionary = {"time": key_time(anim.track_get_key_time(track, key))}
163
+ var keyframe: Dictionary = {"time": held_seconds(anim.track_get_key_time(track, key))}
163
164
  if kind == Animation.TYPE_METHOD:
164
165
  var method: String = str(anim.method_track_get_name(track, key))
165
166
  methods[method] = true
@@ -320,7 +320,10 @@ func create_animation(args: Dictionary) -> Dictionary:
320
320
  return "%s would not be in the scene when it loads." % animation_name
321
321
  return ""
322
322
  var answer: Dictionary = {
323
- "ok": true, "animationName": animation_name, "length": anim.length, "loopMode": loop_mode_name
323
+ "ok": true,
324
+ "animationName": animation_name,
325
+ "length": GdhAnimationReads.held_seconds(anim.length),
326
+ "loopMode": loop_mode_name
324
327
  }
325
328
  if library[2]:
326
329
  answer["libraryMade"] = animation_name.get_slice("/", 0) if animation_name.contains("/") else ""
@@ -477,9 +480,9 @@ func set_animation(args: Dictionary) -> Dictionary:
477
480
  var answer: Dictionary = {
478
481
  "ok": true,
479
482
  "animationName": animation_name,
480
- "length": anim.length,
483
+ "length": GdhAnimationReads.held_seconds(anim.length),
481
484
  "loopMode": GdhAnimationReads.LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
482
- "step": anim.step,
485
+ "step": GdhAnimationReads.held_seconds(anim.step),
483
486
  }
484
487
  var past: String = _past_length(anim, animation_name)
485
488
  if not past.is_empty():
@@ -444,7 +444,7 @@ static func _keys_at(anim: Animation, calling: Array, times: Array) -> Array:
444
444
  if pair[1] >= 0 and key != pair[1]:
445
445
  continue
446
446
  var time: float = anim.track_get_key_time(index, key)
447
- var _listed: bool = held.append(str(GdhAnimationReads.key_time(time)))
447
+ var _listed: bool = held.append(str(GdhAnimationReads.held_seconds(time)))
448
448
  for given: float in times:
449
449
  if GdhAnimationReads.same_time(time, given):
450
450
  at.append([index, key])
@@ -61,9 +61,11 @@ func _init(host: Node, values: GdhValues) -> void:
61
61
  ## pointer when the game is on one.
62
62
  ##
63
63
  ## For the root window the engine asks Windows where the pointer is, and GetCursorPos refuses a
64
- ## process on a desktop that is not in use: measured on one of gdharness's own, it answered false
65
- ## with access denied and left the point unwritten. The engine does not check, so a game driven
66
- ## there read get_mouse_position() as x 836852416 after a motion to 1310, 314. Nothing here can
64
+ ## process on a desktop that is not in use: on one of gdharness's own it answers false and leaves the
65
+ ## point unwritten, with access denied in the user's window station and with
66
+ ## ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION in the one gdharness's desktop is in. The engine does
67
+ ## not check, so a game driven there read get_mouse_position() as x 836852416 after a motion to
68
+ ## 1310, 314. Nothing here can
67
69
  ## put a pointer on that desktop: Windows moves one only on the desktop in use, and making this one
68
70
  ## that would take the screen. What reads the injected event is right, tooltips included, since the
69
71
  ## engine places a tooltip from the motion that reached the control.
@@ -125,15 +125,16 @@ static func no_animation(player: AnimationPlayer, called: String, animation_name
125
125
 
126
126
  static func _summary(anim: Animation) -> Dictionary:
127
127
  return {
128
- "length": anim.length,
128
+ "length": held_seconds(anim.length),
129
129
  "loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
130
- "step": anim.step,
130
+ "step": held_seconds(anim.step),
131
131
  }
132
132
 
133
133
 
134
- ## [param seconds], a key's time, as the shortest decimal holding the same 32-bit float. A scene keeps
135
- ## key times as 32-bit floats, so a key put at 0.7 read back as 0.699999988079071.
136
- static func key_time(seconds: float) -> float:
134
+ ## [param seconds], a key's time or an animation's length or step, as the shortest decimal holding
135
+ ## the same 32-bit float. An animation keeps all three as 32-bit floats, so a key put at 0.7 read
136
+ ## back as 0.699999988079071 and a step of 0.1 as 0.100000001490116.
137
+ static func held_seconds(seconds: float) -> float:
137
138
  var held: float = PackedFloat32Array([seconds])[0]
138
139
  for places: int in range(0, 10):
139
140
  var shorter: float = String.num(held, places).to_float()
@@ -159,7 +160,7 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
159
160
  var keyframes: Array[Dictionary] = []
160
161
  var methods: Dictionary = {}
161
162
  for key: int in anim.track_get_key_count(track):
162
- var keyframe: Dictionary = {"time": key_time(anim.track_get_key_time(track, key))}
163
+ var keyframe: Dictionary = {"time": held_seconds(anim.track_get_key_time(track, key))}
163
164
  if kind == Animation.TYPE_METHOD:
164
165
  var method: String = str(anim.method_track_get_name(track, key))
165
166
  methods[method] = true
@@ -51,6 +51,7 @@ const BEGUN: String = "[gdharness] begun"
51
51
  ## scene or a resource, loads the scripts of the file it opens.
52
52
  const AFTER_THE_AUTOLOADS: Array[String] = [
53
53
  "check_scripts",
54
+ "check_warnings",
54
55
  "create_script",
55
56
  # An exporter made in _init, before an editor's main loop has started, crashed the engine.
56
57
  "export_options",
@@ -99,6 +100,8 @@ func _init() -> void:
99
100
  var params: Dictionary = read
100
101
 
101
102
  _log.info("Executing operation: " + operation)
103
+ if operation == "check_warnings":
104
+ GdhScriptCheck.raise_warnings()
102
105
 
103
106
  var answer_path: String = args[answer_index]
104
107
  if operation in AFTER_THE_AUTOLOADS:
@@ -279,6 +282,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
279
282
  payload = GdhRenameChecks.new(_log).names_taken(params)
280
283
  "check_scripts":
281
284
  payload = GdhScriptCheck.new(_log).check_scripts(params)
285
+ "check_warnings":
286
+ payload = GdhScriptCheck.new(_log).check_warnings(params)
282
287
 
283
288
  # Plugins and input
284
289
  "list_plugins":
@@ -19,6 +19,15 @@ const GdhLog = preload("logger.gd")
19
19
  ## Where the engine raises a script's compile errors from.
20
20
  const RELOAD: String = "GDScript::reload"
21
21
 
22
+ ## What the engine puts on the end of a warning it raised as an error. Kept in step with
23
+ ## `RAISED_WARNING` in src/server.ts.
24
+ const RAISED: String = " (Warning treated as error.)"
25
+
26
+ ## Where the project's warning levels are, each 0 to ignore, 1 to warn and 2 to raise as an error.
27
+ const WARNING_LEVELS: String = "debug/gdscript/warnings/"
28
+ const WARN: int = 1
29
+ const ERROR_LEVEL: int = 2
30
+
22
31
  var _log: GdhLog
23
32
 
24
33
 
@@ -112,3 +121,52 @@ func check_scripts(params: Dictionary) -> Dictionary:
112
121
  break
113
122
  OS.remove_logger(caught)
114
123
  return {"checked": checked, "failed": failed, "scripts": scripts}
124
+
125
+
126
+ ## Raises every warning the project sets to warn so that the engine reports it as an error, which is
127
+ ## the one way it reports a warning outside an editor or a debugger. Not saved. The analyser reads
128
+ ## the levels again only once the settings change has been flushed, at the next frame, so this is
129
+ ## called in `_init` and the scripts are loaded after it; the project's autoloads, compiled before
130
+ ## that frame, compile under the project's own levels. A warning set to ignore stays ignored, and
131
+ ## @warning_ignore and the directory rules hold as they do in the editor, since the analyser applies
132
+ ## them itself.
133
+ static func raise_warnings() -> void:
134
+ for property: Dictionary in ProjectSettings.get_property_list():
135
+ var setting: String = str(property.get("name", ""))
136
+ if (
137
+ not setting.begins_with(WARNING_LEVELS)
138
+ or GdhRead.as_int(property.get("type", TYPE_NIL)) != TYPE_INT
139
+ ):
140
+ continue
141
+ if GdhRead.as_int(ProjectSettings.get_setting(setting)) == WARN:
142
+ ProjectSettings.set_setting(setting, ERROR_LEVEL)
143
+
144
+
145
+ ## The warnings of each script in [code]paths[/code], under the levels [method raise_warnings] left:
146
+ ## [code]scripts[/code] holds each that has any, with its warnings' lines and the engine's messages
147
+ ## as raised. The analyser runs before anything is compiled, so a script whose dependency failed on a
148
+ ## warning still reports its own, and loading on past a failure loses nothing; only what is raised at
149
+ ## the script's own path is its, since a dependency is read on its own entry as well.
150
+ func check_warnings(params: Dictionary) -> Dictionary:
151
+ var asked: Array = params.get("paths", [])
152
+ var caught: GdhCaught = GdhCaught.new()
153
+ OS.add_logger(caught)
154
+ var checked: Array[String] = []
155
+ var scripts: Array[Dictionary] = []
156
+ for entry: Variant in asked:
157
+ var path: String = str(entry)
158
+ checked.append(path)
159
+ caught.raised.clear()
160
+ var _loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
161
+ var warnings: Array[Dictionary] = []
162
+ for error: Dictionary in caught.raised:
163
+ var message: String = str(error["message"])
164
+ if str(error["path"]) != path or not message.ends_with(RAISED):
165
+ continue
166
+ var found: Dictionary = {"line": GdhRead.as_int(error["line"]), "message": message}
167
+ if not found in warnings:
168
+ warnings.append(found)
169
+ if not warnings.is_empty():
170
+ scripts.append({"path": path, "warnings": warnings})
171
+ OS.remove_logger(caught)
172
+ return {"checked": checked, "scripts": scripts}
package/build/keeper.js CHANGED
@@ -7053,7 +7053,18 @@ public static class GdharnessDesktop {
7053
7053
  }
7054
7054
 
7055
7055
  [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
7056
- static extern IntPtr CreateDesktopW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes);
7056
+ static extern IntPtr CreateDesktopExW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes, uint heapKilobytes, IntPtr reserved);
7057
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
7058
+ static extern IntPtr CreateWindowStationW(string name, uint flags, uint access, IntPtr attributes);
7059
+ [DllImport("user32.dll", SetLastError = true)] static extern IntPtr GetProcessWindowStation();
7060
+ [DllImport("user32.dll", SetLastError = true)] static extern bool SetProcessWindowStation(IntPtr station);
7061
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
7062
+ static extern bool GetUserObjectInformationW(IntPtr handle, int index, StringBuilder into, int size, out int needed);
7063
+ const uint WindowStationAllAccess = 0x37F;
7064
+ const int ObjectName = 2;
7065
+ // What Windows gives an interactive desktop. A non-interactive station's desktops get a small
7066
+ // fraction of that by default, and one desktop here holds every engine a run starts.
7067
+ const uint DesktopHeapKilobytes = 20480;
7057
7068
 
7058
7069
  [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
7059
7070
  static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out Started started);
@@ -7144,12 +7155,34 @@ public static class GdharnessDesktop {
7144
7155
  StartupInfo startup = new StartupInfo();
7145
7156
  startup.cb = Marshal.SizeOf(typeof(StartupInfo));
7146
7157
  if (desktop.Length > 0) {
7147
- Step(stepFile, "creating the " + desktop + " desktop");
7148
- IntPtr held = CreateDesktopW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero);
7158
+ Step(stepFile, "opening the session's own window station");
7159
+ IntPtr station = CreateWindowStationW(null, 0, WindowStationAllAccess, IntPtr.Zero);
7160
+ if (station == IntPtr.Zero) {
7161
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateWindowStationW");
7162
+ }
7163
+ StringBuilder stationName = new StringBuilder(256);
7164
+ int needed;
7165
+ if (!GetUserObjectInformationW(station, ObjectName, stationName, stationName.Capacity * 2, out needed)) {
7166
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "GetUserObjectInformationW");
7167
+ }
7168
+ // A desktop is made in the calling process's station, so this one is the caller's while it is.
7169
+ IntPtr own = GetProcessWindowStation();
7170
+ if (!SetProcessWindowStation(station)) {
7171
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "SetProcessWindowStation");
7172
+ }
7173
+ IntPtr held;
7174
+ int made;
7175
+ try {
7176
+ Step(stepFile, "creating the " + desktop + " desktop");
7177
+ held = CreateDesktopExW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero, DesktopHeapKilobytes, IntPtr.Zero);
7178
+ made = Marshal.GetLastWin32Error();
7179
+ } finally {
7180
+ SetProcessWindowStation(own);
7181
+ }
7149
7182
  if (held == IntPtr.Zero) {
7150
- throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateDesktopW " + desktop);
7183
+ throw new Win32Exception(made, "CreateDesktopExW " + stationName + "\\\\" + desktop);
7151
7184
  }
7152
- startup.desktop = desktop;
7185
+ startup.desktop = stationName + "\\\\" + desktop;
7153
7186
  }
7154
7187
  startup.flags = UseStdHandles;
7155
7188
  if (noActivate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.38",
3
+ "version": "1.3.40",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",