gdharness 1.3.39 → 1.3.41

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) {
@@ -12877,7 +12910,7 @@ var init_doc_cache = __esm(() => {
12877
12910
 
12878
12911
  // src/project-scan.ts
12879
12912
  import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync9 } from "node:fs";
12880
- import { basename as basename5, extname, join as join11 } from "node:path";
12913
+ import { basename as basename5, extname, join as join11, relative as relative4, sep as sep3 } from "node:path";
12881
12914
  function skipped(name) {
12882
12915
  return name.startsWith(".") || SKIPPED.has(name);
12883
12916
  }
@@ -13037,6 +13070,20 @@ function pathsInUidCache(projectPath) {
13037
13070
  }
13038
13071
  return new Set(paths.values());
13039
13072
  }
13073
+ function steppedOverAround(projectPath, place) {
13074
+ let at = projectPath;
13075
+ for (const step of relative4(projectPath, place).split(sep3).filter((part) => part !== "")) {
13076
+ at = join11(at, step);
13077
+ if (!statSync9(at).isDirectory()) {
13078
+ break;
13079
+ }
13080
+ const why = skipped(step) ? step.startsWith(".") ? "its name starts with a dot" : `it is ${step}` : gdignored(at) ? "it holds a .gdignore" : installedByUs(at) ? "gdharness installed it" : null;
13081
+ if (why !== null) {
13082
+ return { folder: `res://${relative4(projectPath, at).split(sep3).join("/")}/`, why };
13083
+ }
13084
+ }
13085
+ return null;
13086
+ }
13040
13087
  function isBinary(bytes) {
13041
13088
  return bytes.subarray(0, 8000).includes(0);
13042
13089
  }
@@ -13162,8 +13209,8 @@ function searchProject(projectPath, options) {
13162
13209
  result.summary.files_with_matches += 1;
13163
13210
  }
13164
13211
  };
13165
- const visit = (directory) => {
13166
- if (steppedOver(directory)) {
13212
+ const visit = (directory, named = false) => {
13213
+ if (!named && steppedOver(directory)) {
13167
13214
  if (gdignored(directory)) {
13168
13215
  notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
13169
13216
  }
@@ -13186,20 +13233,17 @@ function searchProject(projectPath, options) {
13186
13233
  };
13187
13234
  const within = options.within ?? projectPath;
13188
13235
  if (statSync9(within).isDirectory()) {
13189
- visit(within);
13236
+ visit(within, true);
13190
13237
  } else {
13191
13238
  searchFile(within, basename5(within));
13192
13239
  }
13193
13240
  const answered = notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
13194
- if (extensions === null || result.summary.files_searched > 0) {
13195
- return answered;
13196
- }
13197
- const asked = [...extensions].join(", ");
13198
- const there = [...met].sort().join(", ");
13199
- return {
13200
- ...answered,
13201
- note: met.size === 0 ? `No file was searched: there is no file there to search.` : `No file was searched: none there has the extension ${asked}. The extensions there are ${there}.`
13202
- };
13241
+ const around = options.within === undefined ? null : steppedOverAround(projectPath, within);
13242
+ const notes = [
13243
+ around === null ? undefined : `${around.folder} is searched because path names it; a search of the whole project steps over it, since ${around.why}.`,
13244
+ extensions === null || result.summary.files_searched > 0 ? undefined : met.size === 0 ? "No file was searched: there is no file there to search." : `No file was searched: none there has the extension ${[...extensions].join(", ")}. The extensions there are ${[...met].sort().join(", ")}.`
13245
+ ].filter((note) => note !== undefined);
13246
+ return notes.length === 0 ? answered : { ...answered, note: notes.join(" ") };
13203
13247
  }
13204
13248
  var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
13205
13249
  var init_project_scan = __esm(() => {
@@ -16400,8 +16444,14 @@ var init_tool_definitions = __esm(() => {
16400
16444
  type: "string",
16401
16445
  description: "Variables: the initial value. Constants: the value, which a constant needs. As written in GDScript."
16402
16446
  },
16403
- isExport: { type: "boolean", description: "Variables: add @export." },
16404
- exportHint: { type: "string", description: 'Variables: the export hint, such as "range(0, 100)".' },
16447
+ isExport: {
16448
+ type: "boolean",
16449
+ description: "Variables: add @export. Default true when exportHint is given."
16450
+ },
16451
+ exportHint: {
16452
+ type: "string",
16453
+ description: 'Variables: the export hint, such as "range(0, 100)", written as @export_range(0, 100). Refused beside isExport false.'
16454
+ },
16405
16455
  isOnready: { type: "boolean", description: "Variables: add @onready." },
16406
16456
  position: {
16407
16457
  type: "string",
@@ -16504,7 +16554,7 @@ var init_tool_definitions = __esm(() => {
16504
16554
  {
16505
16555
  name: "project_settings",
16506
16556
  title: "Project settings",
16507
- description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
16557
+ description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start, and enable_plugin notes a script it would not load. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing, its note naming the nearest settings when the engine does not declare it either. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
16508
16558
  parameters: {
16509
16559
  projectPath: PROJECT_PATH,
16510
16560
  setting: {
@@ -16664,7 +16714,7 @@ var init_tool_definitions = __esm(() => {
16664
16714
  name: "project_search",
16665
16715
  title: "Search the project",
16666
16716
  effect: "read",
16667
- description: "Searches text or a regular expression across project files and returns file paths with line numbers, one entry per line that holds the query, however often it holds it; total_matches counts those lines. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
16717
+ description: "Searches text or a regular expression across project files and returns file paths with line numbers, one entry per line that holds the query, however often it holds it; total_matches counts those lines. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them. A path naming one of those folders, or a place inside one, is searched as asked, and note says the whole-project search steps over it. Each line is searched on its own, so a query holding a line break is refused.",
16668
16718
  parameters: {
16669
16719
  projectPath: PROJECT_PATH,
16670
16720
  query: { type: "string", description: "The text or pattern to find." },
@@ -16679,7 +16729,11 @@ var init_tool_definitions = __esm(() => {
16679
16729
  },
16680
16730
  regex: { type: "boolean", description: "Read the query as a regular expression. Default false." },
16681
16731
  caseSensitive: { type: "boolean", description: "Default false." },
16682
- maxResults: { type: "number", description: "Default 100." }
16732
+ maxResults: {
16733
+ type: "integer",
16734
+ minimum: 1,
16735
+ description: "How many matching lines to answer. Default 100."
16736
+ }
16683
16737
  },
16684
16738
  requires: ["projectPath", "query"]
16685
16739
  },
@@ -16696,7 +16750,11 @@ var init_tool_definitions = __esm(() => {
16696
16750
  enum: ["forward", "reverse"],
16697
16751
  description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
16698
16752
  },
16699
- depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
16753
+ depth: {
16754
+ type: "integer",
16755
+ minimum: 1,
16756
+ description: "forward: how many levels to follow, 1 being what the file loads itself. Default unlimited."
16757
+ },
16700
16758
  includeBuiltin: {
16701
16759
  type: "boolean",
16702
16760
  description: "forward: include the engine's own res://. resources. Default false."
@@ -16873,7 +16931,7 @@ var init_tool_definitions = __esm(() => {
16873
16931
  path: {
16874
16932
  type: ["string", "array"],
16875
16933
  items: { type: "string" },
16876
- description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. Default tests and test, those the project has, which is where gdharness test looks."
16934
+ description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. Default tests and test, those the project has, which is where gdharness test looks.'
16877
16935
  },
16878
16936
  ignore: {
16879
16937
  type: "array",
@@ -16916,7 +16974,7 @@ var init_tool_definitions = __esm(() => {
16916
16974
  rootNodeType: {
16917
16975
  type: "string",
16918
16976
  ops: ["create"],
16919
- description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node."
16977
+ description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node. The answer names the root as scene_node get does: rootNodeType its engine class, with scriptClass for a class_name."
16920
16978
  },
16921
16979
  rootNodeName: {
16922
16980
  type: "string",
@@ -16952,8 +17010,9 @@ var init_tool_definitions = __esm(() => {
16952
17010
  projectPath: PROJECT_PATH,
16953
17011
  scenePath: SCENE_PATH,
16954
17012
  depth: {
16955
- type: "number",
16956
- description: "How many levels to descend. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
17013
+ type: "integer",
17014
+ minimum: 0,
17015
+ description: "How many levels to descend, 0 being the root alone. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
16957
17016
  },
16958
17017
  includeProperties: {
16959
17018
  type: "boolean",
@@ -16961,7 +17020,7 @@ var init_tool_definitions = __esm(() => {
16961
17020
  },
16962
17021
  includeDefaults: {
16963
17022
  type: "boolean",
16964
- description: "With includeProperties, list every stored property, defaults included: a Control has about a hundred, and ten of them ran to fifty-seven thousand characters. Default false."
17023
+ description: "List every stored property, defaults included, which turns includeProperties on when it is left out: a Control has about a hundred, and ten of them ran to fifty-seven thousand characters. Default false."
16965
17024
  }
16966
17025
  },
16967
17026
  requires: ["projectPath", "scenePath"]
@@ -16986,7 +17045,7 @@ var init_tool_definitions = __esm(() => {
16986
17045
  nodeType: {
16987
17046
  type: "string",
16988
17047
  ops: ["add"],
16989
- description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer's nodeType is the class of the node made, for an instance the class of its scene's root.`
17048
+ description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer names the node made as get does: nodeType its engine class, for an instance the class of its scene's root, with scriptClass when its script declares a class_name.`
16990
17049
  },
16991
17050
  instancePath: {
16992
17051
  type: "string",
@@ -17084,7 +17143,7 @@ var init_tool_definitions = __esm(() => {
17084
17143
  {
17085
17144
  name: "scene_signal",
17086
17145
  title: "Scene signals",
17087
- description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17146
+ description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17088
17147
  parameters: {
17089
17148
  projectPath: PROJECT_PATH,
17090
17149
  scenePath: SCENE_PATH,
@@ -17152,7 +17211,7 @@ var init_tool_definitions = __esm(() => {
17152
17211
  animationName: {
17153
17212
  type: "string",
17154
17213
  ops: ["read", "create", "add_track", "remove_track", "set", "rename", "remove", "add_state"],
17155
- description: "The animation in the player. A name before a slash is the library it is in, as moves/walk. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. create: a library the player does not have is made in the scene and named under libraryMade. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
17214
+ description: "The animation in the player. A name before a slash is the library it is in, as moves/walk. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. create: a library the player does not have is made in the scene and named under libraryMade, where an empty name is the default library of the player, the one a name without a slash is in. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
17156
17215
  },
17157
17216
  newName: {
17158
17217
  type: "string",
@@ -17582,7 +17641,7 @@ var init_tool_definitions = __esm(() => {
17582
17641
  type: ["string", "array"],
17583
17642
  items: { type: "string" },
17584
17643
  ops: ["start"],
17585
- description: "start: gdUnit4 suites to play instead of a scene, a suite file or a test directory inside the project or a list of them, as project_test's path takes them. They run in the tests addon's suites scene through the runner gdharness test uses, so with profile: true the profile names what the suites cost by function, which project_test and gdharness test cannot show, since the profiler never sees a script run. The run quits with gdUnit4's exit code; read it with editor_output until running is false, then profile. Takes no scene or executable, and needs gdUnit4 and the tests addon of this release in the project."
17644
+ description: "start: gdUnit4 suites to play instead of a scene, a suite file or a test directory inside the project or a list of them, as project_test's path takes them, though always whole: an entry naming one case is refused. They run in the tests addon's suites scene through the runner gdharness test uses, so with profile: true the profile names what the suites cost by function, which project_test and gdharness test cannot show, since the profiler never sees a script run. The run quits with gdUnit4's exit code; read it with editor_output until running is false, then profile. Takes no scene or executable, and needs gdUnit4 and the tests addon of this release in the project."
17586
17645
  },
17587
17646
  args: {
17588
17647
  type: "array",
@@ -17604,7 +17663,7 @@ var init_tool_definitions = __esm(() => {
17604
17663
  visible: {
17605
17664
  type: "boolean",
17606
17665
  ops: ["start"],
17607
- 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."
17666
+ 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."
17608
17667
  },
17609
17668
  silent: {
17610
17669
  type: "boolean",
@@ -18386,7 +18445,7 @@ var init_tool_definitions = __esm(() => {
18386
18445
  {
18387
18446
  name: "debug_breakpoint",
18388
18447
  title: "Breakpoints",
18389
- description: "Sets, removes or lists breakpoints. A set on a script that is not there, past its last line, or on a line holding no code, blank or only a comment, is refused, as nothing would ever stop there; a remove takes any line, and with no editor connected answers removed false, saying why, when none was held there. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, for a headless run of a project whose editor plays its games with a window, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it, and the set answers once the game has taken it, which it does between frames, so a call made after the answer stops there (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
18448
+ description: "Sets, removes or lists breakpoints. A set on a script that is not there, past its last line, or on a line holding no code, blank or only a comment, is refused, as nothing would ever stop there; a remove takes any line, and with no editor connected answers removed false, saying why, when none was held there. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, for a headless run of a project whose editor plays its games with a window, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it, and the set answers once the game has taken it, which it does between frames, so a call made after the answer stops there (runningGame says so). Such a game does not stop on a script error, so a run that meets one carries on past it: editor_output has the error with where it happened, and a breakpoint on that line stops the game before the line runs. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
18390
18449
  parameters: {
18391
18450
  projectPath: PROJECT_PATH,
18392
18451
  scriptPath: { ...SCRIPT_PATH, ops: ["set", "remove"] },
@@ -41398,7 +41457,7 @@ var init_boot_note = __esm(() => {
41398
41457
 
41399
41458
  // src/breakpoint-note.ts
41400
41459
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync13, writeFileSync as writeFileSync14 } from "node:fs";
41401
- import { dirname as dirname16, isAbsolute as isAbsolute7, join as join31, relative as relative5 } from "node:path";
41460
+ import { dirname as dirname16, isAbsolute as isAbsolute7, join as join31, relative as relative6 } from "node:path";
41402
41461
  function breakpointNotePath(projectPath) {
41403
41462
  return join31(projectPath, ".godot", "gdharness-breakpoints.json");
41404
41463
  }
@@ -41413,7 +41472,7 @@ function writeBreakpointNote(projectPath, held) {
41413
41472
  }
41414
41473
  const note = {
41415
41474
  breakpoints: inside.map((one) => ({
41416
- script: relative5(root, one.scriptPath).replaceAll("\\", "/"),
41475
+ script: relative6(root, one.scriptPath).replaceAll("\\", "/"),
41417
41476
  lines: [...one.lines]
41418
41477
  }))
41419
41478
  };
@@ -41450,7 +41509,7 @@ function readBreakpointNote(projectPath) {
41450
41509
  }
41451
41510
  }
41452
41511
  function withinProject(projectPath, scriptPath) {
41453
- const rel = relative5(projectPath, scriptPath);
41512
+ const rel = relative6(projectPath, scriptPath);
41454
41513
  return rel !== "" && !rel.startsWith("..") && !isAbsolute7(rel);
41455
41514
  }
41456
41515
  var init_breakpoint_note = __esm(() => {
@@ -43343,9 +43402,9 @@ var init_rename = __esm(() => {
43343
43402
 
43344
43403
  // src/rename-symbol.ts
43345
43404
  import { existsSync as existsSync30, mkdirSync as mkdirSync17, readdirSync as readdirSync16, realpathSync as realpathSync4, rmSync as rmSync16, writeFileSync as writeFileSync18 } from "node:fs";
43346
- import { basename as basename9, dirname as dirname21, join as join36, relative as relative6, resolve as resolve12 } from "node:path";
43405
+ import { basename as basename9, dirname as dirname21, join as join36, relative as relative7, resolve as resolve12 } from "node:path";
43347
43406
  function resourcePathOf(projectPath, absolute) {
43348
- const inside = relative6(onDiskSpelling(projectPath), onDiskSpelling(absolute));
43407
+ const inside = relative7(onDiskSpelling(projectPath), onDiskSpelling(absolute));
43349
43408
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
43350
43409
  return null;
43351
43410
  }
@@ -43851,7 +43910,7 @@ import {
43851
43910
  statSync as statSync17,
43852
43911
  writeFileSync as writeFileSync19
43853
43912
  } from "node:fs";
43854
- import { basename as basename10, dirname as dirname22, extname as extname3, join as join37, relative as relative7 } from "node:path";
43913
+ import { basename as basename10, dirname as dirname22, extname as extname3, join as join37, relative as relative8 } from "node:path";
43855
43914
  function filesUnder(directory) {
43856
43915
  return readdirSync17(directory, { withFileTypes: true }).flatMap((entry) => {
43857
43916
  const path = join37(directory, entry.name);
@@ -43912,7 +43971,7 @@ function binaryResources(projectPath) {
43912
43971
  return found;
43913
43972
  }
43914
43973
  function targetPathOf(projectPath, absolute) {
43915
- const inside = relative7(onDiskSpelling(projectPath), join37(onDiskSpelling(dirname22(absolute)), basename10(absolute)));
43974
+ const inside = relative8(onDiskSpelling(projectPath), join37(onDiskSpelling(dirname22(absolute)), basename10(absolute)));
43916
43975
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
43917
43976
  return null;
43918
43977
  }
@@ -47875,7 +47934,14 @@ function similarity(one, other) {
47875
47934
  const shared = ones.filter((pair) => others.includes(pair)).length;
47876
47935
  return 2 * shared / (ones.length + others.length);
47877
47936
  }
47878
- function withNearestFiles(said, wanted, files) {
47937
+ function withNearestFiles(said, wanted, files, beside) {
47938
+ const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
47939
+ const named = said.split(/[\s,;]+/).map((word) => word.replace(/[.:]+$/, ""));
47940
+ const namesakes = beside.filter((file) => file !== wanted && stem(file) === stem(wanted) && !named.includes(file)).sort();
47941
+ if (namesakes.length > 0) {
47942
+ const alone = namesakes.length === 1;
47943
+ return `${said}. The ${alone ? "file" : "files"} of that name beside it ${alone ? "is" : "are"} ${namesakes.join(", ")}.`;
47944
+ }
47879
47945
  const extension = /\.([^./]+)$/.exec(wanted)?.[1];
47880
47946
  if (extension === undefined) {
47881
47947
  return said;
@@ -47883,7 +47949,6 @@ function withNearestFiles(said, wanted, files) {
47883
47949
  if (files.length === 0) {
47884
47950
  return `${said}. The project holds no .${extension} file.`;
47885
47951
  }
47886
- const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
47887
47952
  const close = nearest(stem(wanted), files.map((file) => stem(file)));
47888
47953
  const offered = files.filter((file) => close.includes(stem(file)));
47889
47954
  if (offered.length > 0) {
@@ -48861,7 +48926,7 @@ var init_server_instructions = __esm(() => {
48861
48926
 
48862
48927
  // src/suites-scene.ts
48863
48928
  import { existsSync as existsSync32, mkdirSync as mkdirSync22, writeFileSync as writeFileSync21 } from "node:fs";
48864
- import { join as join41, relative as relative8, sep as sep4 } from "node:path";
48929
+ import { join as join41, relative as relative9, sep as sep5 } from "node:path";
48865
48930
  function hasSuitesScene(projectPath) {
48866
48931
  return existsSync32(join41(projectPath, SUITES_SCENE));
48867
48932
  }
@@ -48885,7 +48950,7 @@ function suitesSceneArgs(projectPath, suites) {
48885
48950
  "-c",
48886
48951
  "--ignoreHeadlessMode",
48887
48952
  "-rd",
48888
- `res://${relative8(projectPath, join41(directory, "reports")).split(sep4).join("/")}`,
48953
+ `res://${relative9(projectPath, join41(directory, "reports")).split(sep5).join("/")}`,
48889
48954
  "-rc",
48890
48955
  "1"
48891
48956
  ];
@@ -48900,6 +48965,44 @@ var init_suites_scene = __esm(() => {
48900
48965
  // src/test-selection.ts
48901
48966
  import { existsSync as existsSync33 } from "node:fs";
48902
48967
  import { basename as basename12, join as join42 } from "node:path";
48968
+ function casesOutOfPath(projectPath, given, tests) {
48969
+ const entries = Array.isArray(given) ? given : [given];
48970
+ const cases = [];
48971
+ const whole = [];
48972
+ const path = entries.map((entry) => {
48973
+ const caseAt = typeof entry === "string" ? CASE_AFTER_SCRIPT.exec(entry.trim()) : null;
48974
+ const suite = caseAt === null ? null : resolveWithinProject(projectPath, caseAt[1] ?? "");
48975
+ if (caseAt === null || suite === null || !suite.ok) {
48976
+ if (typeof entry === "string") {
48977
+ whole.push(entry);
48978
+ }
48979
+ return entry;
48980
+ }
48981
+ cases.push(`res://${suite.relativePath}:${caseAt[2] ?? ""}`);
48982
+ return `res://${suite.relativePath}`;
48983
+ });
48984
+ if (cases.length === 0) {
48985
+ return { ok: true, path: given, tests };
48986
+ }
48987
+ if (whole.length > 0) {
48988
+ return {
48989
+ ok: false,
48990
+ reason: `path names ${cases.length === 1 ? "a case" : "cases"}, ${cases.join(", ")}, beside ${whole.join(", ")}, and a run with cases named runs only those: run the cases on their own, and the rest in a run of its own.`
48991
+ };
48992
+ }
48993
+ const named = tests === undefined || tests === null ? [] : Array.isArray(tests) ? tests : [tests];
48994
+ return { ok: true, path: [...new Set(path)], tests: [...named, ...cases] };
48995
+ }
48996
+ function caseInSuites(given) {
48997
+ const entries = Array.isArray(given) ? given : [given];
48998
+ const cased = entries.filter((entry) => typeof entry === "string" && CASE_AFTER_SCRIPT.test(entry.trim()));
48999
+ const first = cased[0];
49000
+ if (first === undefined) {
49001
+ return null;
49002
+ }
49003
+ const file = CASE_AFTER_SCRIPT.exec(first.trim())?.[1] ?? first;
49004
+ return `suites plays whole suites, and ${cased.join(", ")} ${cased.length === 1 ? "names" : "name"} one case: name the file, ${file}, to play every case in it, or run the case alone with project_test, whose path takes ${first}.`;
49005
+ }
48903
49006
  function casesIn(scripts, suite) {
48904
49007
  return scripts.suite(suite)?.cases ?? scripts.script(suite)?.funcs.filter((name) => name.startsWith("test_")) ?? [];
48905
49008
  }
@@ -48961,9 +49064,12 @@ function selectTests(projectPath, asked, given) {
48961
49064
  const ignore = declared.filter((one) => suites.includes(one.suite) && !chosen.has(`${one.suite}:${one.test}`)).map((one) => `${one.suite}:${one.test}`);
48962
49065
  return { ok: true, value: { suites, ignore, selected } };
48963
49066
  }
49067
+ var CASE_AFTER_SCRIPT;
48964
49068
  var init_test_selection = __esm(() => {
49069
+ init_paths();
48965
49070
  init_project_scan();
48966
49071
  init_suite_plan();
49072
+ CASE_AFTER_SCRIPT = /^(.*\.gd):(\w+)$/;
48967
49073
  });
48968
49074
 
48969
49075
  // src/server.ts
@@ -48985,7 +49091,7 @@ import {
48985
49091
  statSync as statSync20,
48986
49092
  writeFileSync as writeFileSync22
48987
49093
  } from "node:fs";
48988
- import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join43, normalize as normalize2, relative as relative9 } from "node:path";
49094
+ import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join43, normalize as normalize2, relative as relative10 } from "node:path";
48989
49095
  import { setTimeout as delay7 } from "node:timers/promises";
48990
49096
  import { fileURLToPath as fileURLToPath5 } from "node:url";
48991
49097
  function editorRestartTimeoutMs() {
@@ -49142,7 +49248,7 @@ function keptGameRequest(tool, op, args) {
49142
49248
  }
49143
49249
  }
49144
49250
  function projectSpelling(projectPath, absolutePath) {
49145
- return `res://${relative9(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
49251
+ return `res://${relative10(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
49146
49252
  }
49147
49253
  function whichGame(args) {
49148
49254
  return { projectPath: args["projectPath"], pid: args["pid"] };
@@ -50049,7 +50155,7 @@ function failedVerdict(report) {
50049
50155
  const last = parts.pop();
50050
50156
  return last === undefined ? "failures" : parts.length === 0 ? last : `${parts.join(", ")} and ${last}`;
50051
50157
  }
50052
- function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
50158
+ function timedOutVerdict(timeoutMs, silentForMs, repeating = null, printedAnything = true) {
50053
50159
  const quiet = Math.round(silentForMs);
50054
50160
  if (repeating !== null) {
50055
50161
  return {
@@ -50057,6 +50163,12 @@ function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
50057
50163
  hung: false
50058
50164
  };
50059
50165
  }
50166
+ if (!printedAnything && timeoutMs < STALLED_AFTER_MS) {
50167
+ return {
50168
+ verdict: `timed out after ${timeoutMs} ms before the engine printed anything, so it may still have been starting: timeoutMs counts milliseconds, and the default is ${TEST_TIMEOUT_MS}`,
50169
+ hung: false
50170
+ };
50171
+ }
50060
50172
  return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
50061
50173
  verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
50062
50174
  hung: true
@@ -51285,13 +51397,20 @@ class GodotServer {
51285
51397
  return await this.handleRunTests(args);
51286
51398
  case "scene_create":
51287
51399
  return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
51288
- case "scene_tree":
51400
+ case "scene_tree": {
51401
+ const defaults = readBoolean(args, "includeDefaults") ?? false;
51402
+ const asked = readBoolean(args, "includeProperties");
51403
+ if (defaults && asked === false) {
51404
+ return this.createErrorResponse("includeDefaults lists the defaults among the properties, and includeProperties false lists none: leave includeProperties out, or set it true.");
51405
+ }
51406
+ const properties = asked ?? defaults;
51289
51407
  return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
51290
51408
  scenePath: file,
51291
51409
  depth: readNumber(args, "depth") ?? -1,
51292
- includeProperties: readBoolean(args, "includeProperties") ?? false,
51293
- includeDefaults: readBoolean(args, "includeDefaults") ?? false
51294
- }), async () => await bridge("list_scene_nodes"));
51410
+ includeProperties: properties,
51411
+ includeDefaults: defaults
51412
+ }), async () => await bridge("list_scene_nodes", { includeProperties: properties }));
51413
+ }
51295
51414
  case "scene_node":
51296
51415
  switch (op) {
51297
51416
  case "add":
@@ -52448,6 +52567,9 @@ class GodotServer {
52448
52567
  if (!project.ok) {
52449
52568
  return project.response;
52450
52569
  }
52570
+ if (/[\r\n]/.test(readString(args, "query") ?? "")) {
52571
+ return this.createErrorResponse("query holds a line break, and project_search matches one line at a time: search for one of its lines.");
52572
+ }
52451
52573
  const given = readNonEmptyString(args, "path");
52452
52574
  const place = given === undefined ? null : resolveWithinProject(project.value.path, given, { root: true });
52453
52575
  if (place !== null && !place.ok) {
@@ -52697,10 +52819,18 @@ ${platform}`;
52697
52819
  ticker.unref();
52698
52820
  return ticker;
52699
52821
  }
52700
- async handleRunTests(args) {
52822
+ async handleRunTests(given) {
52823
+ const project = this.project(given);
52824
+ if (!project.ok) {
52825
+ return project.response;
52826
+ }
52827
+ const split = casesOutOfPath(project.value.path, given["path"], given["tests"]);
52828
+ if (!split.ok) {
52829
+ return this.createErrorResponse(split.reason);
52830
+ }
52831
+ const args = split.path === given["path"] ? given : { ...given, path: split.path, tests: split.tests };
52701
52832
  const ran = await this.runTests(args);
52702
- const project = this.project(args);
52703
- if (!project.ok || callSignal()?.aborted === true) {
52833
+ if (callSignal()?.aborted === true) {
52704
52834
  return ran;
52705
52835
  }
52706
52836
  const paths = testPathsIn(project.value.path, args["path"]);
@@ -52859,7 +52989,7 @@ ${platform}`;
52859
52989
  "--expected-out",
52860
52990
  expectedFile
52861
52991
  ];
52862
- const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
52992
+ const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? TEST_TIMEOUT_MS;
52863
52993
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
52864
52994
  const helperFiles = {
52865
52995
  spec: join43(userData, "spec"),
@@ -52872,12 +53002,15 @@ ${platform}`;
52872
53002
  const run = await this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
52873
53003
  const console2 = [];
52874
53004
  let lastSaid = Date.now();
53005
+ let saidAnything = false;
52875
53006
  run.process.stdout?.on("data", (data) => {
52876
53007
  console2.push(data);
52877
53008
  lastSaid = Date.now();
53009
+ saidAnything = true;
52878
53010
  });
52879
53011
  run.process.stderr?.on("data", () => {
52880
53012
  lastSaid = Date.now();
53013
+ saidAnything = true;
52881
53014
  });
52882
53015
  const reporting = this.reportTestProgress(run.log);
52883
53016
  const autoloaded = scriptsAutoloaded(project.value.path);
@@ -52970,7 +53103,7 @@ ${platform}`;
52970
53103
  }).entries;
52971
53104
  const said = printed.map((entry) => entry.text);
52972
53105
  const nothingRan = timedOut || notCompiled !== null ? null : whyNoReport(said, asked.join(" and "));
52973
- const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry))) : null;
53106
+ const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry)), saidAnything) : null;
52974
53107
  const hung = cutShort?.hung ?? false;
52975
53108
  const printedErrors = notCompiled !== null ? compileErrorsIn(run.log.all) : timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
52976
53109
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
@@ -53323,7 +53456,10 @@ ${error.text}`;
53323
53456
  missingScript(projectPath, relativePath) {
53324
53457
  const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
53325
53458
  const wanted = `res://${relativePath}`;
53326
- return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts));
53459
+ const folder = dirname25(relativePath.replaceAll("\\", "/"));
53460
+ const inFolder = join43(projectPath, folder);
53461
+ const beside = existsSync34(inFolder) ? readdirSync19(inFolder, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `res://${folder === "." ? "" : `${folder}/`}${entry.name}`) : [];
53462
+ return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts, beside));
53327
53463
  }
53328
53464
  async compileScripts(projectPath, paths) {
53329
53465
  const payload = { checked: [], failed: [], scripts: [] };
@@ -54404,6 +54540,10 @@ ${error.text}`;
54404
54540
  if (args["scene"] !== undefined || args["executable"] !== undefined) {
54405
54541
  return refused("suites plays the suites in the tests addon's own scene, so it takes no scene and no executable: leave those out.");
54406
54542
  }
54543
+ const cased = caseInSuites(args["suites"]);
54544
+ if (cased !== null) {
54545
+ return refused(cased);
54546
+ }
54407
54547
  const paths = testPathsIn(projectPath, args["suites"], "suites");
54408
54548
  if (!paths.ok) {
54409
54549
  return refused(paths.reason, PATH_SOLUTIONS);
@@ -54593,6 +54733,9 @@ ${error.text}`;
54593
54733
  let editorAskedAt = 0;
54594
54734
  let editorSaysGoing = true;
54595
54735
  let editorAsks = 0;
54736
+ let holding = null;
54737
+ let keeperAskedAt = 0;
54738
+ let heldHere = null;
54596
54739
  const waitBegan = Date.now();
54597
54740
  const runBegan = Math.min(this.activeProcess?.startedAt ?? waitBegan, waitBegan);
54598
54741
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
@@ -54605,7 +54748,19 @@ ${error.text}`;
54605
54748
  }
54606
54749
  const run = this.activeProcess;
54607
54750
  if (run === null || !run.throughEditor) {
54608
- return !stillRunning(run);
54751
+ if (!stillRunning(run)) {
54752
+ return true;
54753
+ }
54754
+ const keeper = run?.keeper;
54755
+ if (keeper !== undefined && holding === null && Date.now() - keeperAskedAt >= RUN_POLL_MS) {
54756
+ keeperAskedAt = Date.now();
54757
+ holding = this.keptHold(keeper).then((held) => {
54758
+ heldHere = held;
54759
+ }).finally(() => {
54760
+ holding = null;
54761
+ });
54762
+ }
54763
+ return heldHere !== null;
54609
54764
  }
54610
54765
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
54611
54766
  editorAskedAt = Date.now();
@@ -54641,7 +54796,7 @@ ${error.text}`;
54641
54796
  addon: true,
54642
54797
  budgetMs,
54643
54798
  ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
54644
- heldAt: this.dapClient?.whereItStopped() ?? null,
54799
+ heldAt: going === null || going.throughEditor ? this.dapClient?.whereItStopped() ?? null : endpoint === null && running && going.keeper !== undefined ? await this.keptHold(going.keeper) : null,
54645
54800
  running,
54646
54801
  withArgs,
54647
54802
  servesAProject: this.ownProject !== null,
@@ -56729,7 +56884,12 @@ ${error.text}`;
56729
56884
  return contained.response;
56730
56885
  }
56731
56886
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
56732
- const created = await this.operation("create_script", params, project.value.path);
56887
+ const stale = await this.refreshStaleClasses(project.value.path);
56888
+ if (!stale.ok) {
56889
+ return stale.response;
56890
+ }
56891
+ const loaded = await this.operation("create_script", params, project.value.path);
56892
+ const created = loaded.ok && stale.value.length > 0 ? { ...loaded, payload: { ...loaded.payload, classesRebuiltFirst: stale.value } } : loaded;
56733
56893
  const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
56734
56894
  if (!created.ok || declared === "") {
56735
56895
  return this.answer(created);
@@ -56766,8 +56926,26 @@ ${error.text}`;
56766
56926
  });
56767
56927
  }
56768
56928
  async handleModifyScript(args) {
56769
- const written = await this.headless("modify_script", args);
56770
56929
  const project = this.project(args);
56930
+ const stale = project.ok ? await this.refreshStaleClasses(project.value.path) : null;
56931
+ if (stale !== null && !stale.ok) {
56932
+ return stale.response;
56933
+ }
56934
+ const loaded = await this.headless("modify_script", args);
56935
+ const first = loaded.content[0];
56936
+ const written = stale === null || stale.value.length === 0 || loaded.isError === true || first?.type !== "text" || !first.text.trimStart().startsWith("{") ? loaded : {
56937
+ ...loaded,
56938
+ content: [
56939
+ {
56940
+ ...first,
56941
+ text: answerJson({
56942
+ ...asParams(JSON.parse(first.text)),
56943
+ classesRebuiltFirst: stale.value
56944
+ })
56945
+ },
56946
+ ...loaded.content.slice(1)
56947
+ ]
56948
+ };
56771
56949
  const scriptPath = readNonEmptyString(args, "scriptPath");
56772
56950
  if (written.isError === true || !project.ok || scriptPath === undefined || !this.editorHasOpen(project.value.path)) {
56773
56951
  return written;
@@ -57744,7 +57922,7 @@ async function runGodotServer() {
57744
57922
  const server = new GodotServer;
57745
57923
  await server.run();
57746
57924
  }
57747
- 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;
57925
+ 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_TIMEOUT_MS = 600000, 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;
57748
57926
  var init_server = __esm(() => {
57749
57927
  init_dist2();
57750
57928
  init_stdio();
@@ -58235,7 +58413,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
58235
58413
  These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
58236
58414
  window and refuses headless rather than handing back the last frame anything drew. On Windows a
58237
58415
  windowed run this server starts is on a desktop of its own, where it draws and can be captured but
58238
- never shows or takes the keyboard.
58416
+ never shows or takes the keyboard or the pointer.
58239
58417
 
58240
58418
  The autoload reads these settings and no others. \`${BIND_ADDRESS_SETTING}\`
58241
58419
  is \`127.0.0.1\` and should stay there: the command set includes \`call_method\`, \`set_property\`
@@ -58748,7 +58926,7 @@ import {
58748
58926
  writeFileSync as writeFileSync12
58749
58927
  } from "node:fs";
58750
58928
  import { availableParallelism as availableParallelism4 } from "node:os";
58751
- import { dirname as dirname13, join as join27, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
58929
+ import { dirname as dirname13, join as join27, relative as relative5, resolve as resolve10, sep as sep4 } from "node:path";
58752
58930
  import process14 from "node:process";
58753
58931
 
58754
58932
  // src/suite-fingerprint.ts
@@ -58913,6 +59091,7 @@ function getLocalVersion() {
58913
59091
  }
58914
59092
 
58915
59093
  // src/suite-run.ts
59094
+ var LAST_RUN_FILE = "last-run.json";
58916
59095
  var SLOWEST_NAMED = 5;
58917
59096
  var SILENCE_TIMES_TAKEN = 4;
58918
59097
  function silenceWhile(running, times, floorSeconds) {
@@ -59083,14 +59262,14 @@ class RunLogs {
59083
59262
  return this.kept ?? this.directory;
59084
59263
  }
59085
59264
  named(path) {
59086
- return this.kept === null ? path : join27(this.kept, relative4(this.directory, path));
59265
+ return this.kept === null ? path : join27(this.kept, relative5(this.directory, path));
59087
59266
  }
59088
59267
  keep() {
59089
59268
  if (this.kept === null) {
59090
59269
  return;
59091
59270
  }
59092
59271
  for (const file of readdirSync14(this.directory, { recursive: true, encoding: "utf8" })) {
59093
- if (file.endsWith(".log") && file.split(sep3).length === 2) {
59272
+ if (file.endsWith(".log") && file.split(sep4).length === 2) {
59094
59273
  mkdirSync10(dirname13(join27(this.kept, file)), { recursive: true });
59095
59274
  copyFileSync(join27(this.directory, file), join27(this.kept, file));
59096
59275
  }
@@ -59108,6 +59287,7 @@ async function runTestCommand(options, godotPath) {
59108
59287
  const state = join27(home, ".godot", "gdharness");
59109
59288
  const timesFile = join27(state, "suite-times.json");
59110
59289
  const passedFile = join27(state, "suite-passed.json");
59290
+ const lastRunFile = join27(state, LAST_RUN_FILE);
59111
59291
  let suites = selectedSuites(scripts, options);
59112
59292
  const uncollected = uncollectedSaid(scripts.uncollectedUnder(searchedRoots(options)));
59113
59293
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
@@ -59155,7 +59335,7 @@ async function runTestCommand(options, godotPath) {
59155
59335
  const imported = await importProject(launch);
59156
59336
  if (imported !== null) {
59157
59337
  logs.keep();
59158
- writeEarlyResult(options.resultFile, startedAt, [`the import failed: ${imported}`], logs.shown);
59338
+ writeEarlyResult({ resultFile: options.resultFile, lastRunFile, engines: options.engines }, startedAt, [`the import failed: ${imported}`], logs.shown);
59159
59339
  console.log(`The import failed, so no suite ran: ${imported}`);
59160
59340
  return 1;
59161
59341
  }
@@ -59180,7 +59360,7 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59180
59360
  if (suites.length === 0) {
59181
59361
  console.log("Nothing has changed since every suite last passed.");
59182
59362
  rmSync12(runDirectory, { recursive: true, force: true });
59183
- writeEarlyResult(options.resultFile, startedAt, uncollected, null);
59363
+ writeEarlyResult({ resultFile: options.resultFile, lastRunFile, engines: options.engines }, startedAt, uncollected, null);
59184
59364
  if (uncollected.length === 0) {
59185
59365
  return 0;
59186
59366
  }
@@ -59206,26 +59386,31 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59206
59386
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
59207
59387
  const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
59208
59388
  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)}`;
59209
- console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59389
+ const dealing = [];
59390
+ const say = (line) => {
59391
+ dealing.push(line);
59392
+ console.log(line);
59393
+ };
59394
+ say(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59210
59395
  const windowedAnyway = headlessInAWindow(plans, units);
59211
59396
  if (windowedAnyway.length > 0) {
59212
59397
  const one = windowedAnyway.length === 1;
59213
- console.log(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
59398
+ say(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
59214
59399
  }
59215
59400
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
59216
59401
  if (plans.length > 1 && split.length > 0) {
59217
- console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59402
+ say(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59218
59403
  }
59219
59404
  const byRows = [
59220
59405
  ...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
59221
59406
  ];
59222
59407
  if (plans.length > 1 && byRows.length > 0) {
59223
- console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59408
+ say(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59224
59409
  }
59225
59410
  const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
59226
59411
  if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
59227
59412
  const one = keptWhole.length === 1;
59228
- console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59413
+ say(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59229
59414
  }
59230
59415
  const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
59231
59416
  const taken = [];
@@ -59238,11 +59423,11 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59238
59423
  }
59239
59424
  const shared = options.order === null && lanes.length > 1 && taken.length < lanes.length;
59240
59425
  if (shared) {
59241
- console.log(sharesSaid(taken.length, lanes.length));
59426
+ say(sharesSaid(taken.length, lanes.length));
59242
59427
  }
59243
59428
  let ran = shared ? await runShares(launch, units, declared, lanes.length, taken, times) : (await Promise.all(lanes.map((lane, at) => runLane(launch, lane, times, taken[at] ?? null)))).flat().sort((a, b) => a.index - b.index);
59244
59429
  if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
59245
- console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
59430
+ say("An engine ran out of memory, so every engine that failed runs again, one at a time.");
59246
59431
  const again = [];
59247
59432
  for (const engine of ran) {
59248
59433
  again.push(engine.verdict.passed ? engine : await runEngine2(launch, engine.plan, engine.index, times, "again"));
@@ -59252,7 +59437,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59252
59437
  writeJson(timesFile, timesAfter(readTimes(timesFile), wholeSuites, ran.map((engine) => engine.printed), (path) => scripts.suite(path), new Set(ran.flatMap((engine) => engine.plan.suites.filter((suite) => (engine.printed.suites.get(suite.path)?.closed ?? null) === null).flatMap((suite) => suite.cases.map((func) => `${suite.path}:${func}`))))));
59253
59438
  const across = rowsAcross(ran.map((engine) => ({ handed: engine.plan.suites, printed: engine.printed })));
59254
59439
  recordPassed(passedFile, ran, printsOf, new Set(across.map((fault) => fault.suite)));
59255
- return report(ran, across.map((fault) => fault.fault), uncollected, startedAt, logs, options.printOutput, options.resultFile);
59440
+ return report(ran, across.map((fault) => fault.fault), uncollected, startedAt, logs, options.printOutput, {
59441
+ resultFile: options.resultFile,
59442
+ lastRunFile,
59443
+ dealing,
59444
+ engines: options.engines,
59445
+ slotsFree: taken.length
59446
+ });
59256
59447
  } finally {
59257
59448
  process14.off("SIGINT", interrupted);
59258
59449
  process14.off("SIGTERM", interrupted);
@@ -59417,7 +59608,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
59417
59608
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
59418
59609
  "-c",
59419
59610
  "-rd",
59420
- `res://${relative4(launch.projectPath, join27(directory, "reports")).split(sep3).join("/")}`,
59611
+ `res://${relative5(launch.projectPath, join27(directory, "reports")).split(sep4).join("/")}`,
59421
59612
  "-rc",
59422
59613
  "1",
59423
59614
  "--",
@@ -59459,6 +59650,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
59459
59650
  printed,
59460
59651
  verdict: written === null ? judged : { ...judged, passed: false, faults: [...judged.faults, written] },
59461
59652
  log,
59653
+ began,
59462
59654
  seconds: (Date.now() - began) / 1000,
59463
59655
  again: again !== ""
59464
59656
  };
@@ -59469,11 +59661,11 @@ function reportsFault(reports) {
59469
59661
  if (files.length === 0) {
59470
59662
  return null;
59471
59663
  }
59472
- const shown = files.slice(0, REPORT_FILES_NAMED).map((file) => file.split(sep3).join("/"));
59664
+ const shown = files.slice(0, REPORT_FILES_NAMED).map((file) => file.split(sep4).join("/"));
59473
59665
  const more = files.length - shown.length;
59474
59666
  return `gdUnit4 wrote ${plural(files.length, "file")} into ${reports} (${shown.join(", ")}${more > 0 ? ` and ${String(more)} more` : ""}), a report the runner had turned off, so the gdUnit4 installed no longer honours a session hook marked off; report it to gdharness with gdUnit4's version`;
59475
59667
  }
59476
- function report(ran, across, uncollected, startedAt, logs, printOutput, resultFile) {
59668
+ function report(ran, across, uncollected, startedAt, logs, printOutput, targets) {
59477
59669
  if (printOutput) {
59478
59670
  for (const engine of ran) {
59479
59671
  console.log("");
@@ -59550,27 +59742,31 @@ ${readFileSync22(engine.log, "utf8")}`);
59550
59742
  } else {
59551
59743
  logs.keep();
59552
59744
  }
59553
- if (resultFile !== null) {
59554
- writeJson(resultFile, {
59555
- passed,
59556
- seconds: took,
59557
- problems: [
59558
- ...failed.flatMap((engine) => engine.verdict.faults.map((fault) => `engine ${String(engine.index)}: ${fault}`)),
59559
- ...across,
59560
- ...uncollected
59561
- ],
59562
- suites: closed,
59563
- engines: ran.map((engine) => ({
59564
- index: engine.index,
59565
- seconds: engine.seconds,
59566
- dealt: engine.plan.cost,
59567
- passed: engine.verdict.passed,
59568
- windowed: engine.plan.windowed,
59569
- log: passed ? null : logs.named(engine.log)
59570
- })),
59571
- logs: passed ? null : logs.shown
59572
- });
59573
- }
59745
+ writeResult(targets, {
59746
+ passed,
59747
+ seconds: took,
59748
+ problems: [
59749
+ ...failed.flatMap((engine) => engine.verdict.faults.map((fault) => `engine ${String(engine.index)}: ${fault}`)),
59750
+ ...across,
59751
+ ...uncollected
59752
+ ],
59753
+ suites: closed,
59754
+ engines: ran.map((engine) => ({
59755
+ index: engine.index,
59756
+ lane: engine.plan.lane,
59757
+ startedAfter: (engine.began - startedAt) / 1000,
59758
+ seconds: engine.seconds,
59759
+ dealt: engine.plan.cost,
59760
+ passed: engine.verdict.passed,
59761
+ windowed: engine.plan.windowed,
59762
+ suites: engine.plan.suites.map((suite) => suite.path),
59763
+ log: passed ? null : logs.named(engine.log)
59764
+ })),
59765
+ logs: passed ? null : logs.shown,
59766
+ enginesAsked: targets.engines,
59767
+ slotsFree: targets.slotsFree,
59768
+ dealing: targets.dealing
59769
+ });
59574
59770
  if (passed) {
59575
59771
  console.log(`Passed: ${totals}`);
59576
59772
  return 0;
@@ -59580,16 +59776,23 @@ ${readFileSync22(engine.log, "utf8")}`);
59580
59776
  console.log(`Every engine's output is kept under ${logs.shown} until ${String(KEPT_RUN_DIRECTORIES)} later runs have failed.`);
59581
59777
  return 1;
59582
59778
  }
59583
- function writeEarlyResult(file, startedAt, problems, logs) {
59584
- if (file !== null) {
59585
- writeJson(file, {
59586
- passed: problems.length === 0,
59587
- seconds: (Date.now() - startedAt) / 1000,
59588
- problems,
59589
- suites: [],
59590
- engines: [],
59591
- logs
59592
- });
59779
+ function writeEarlyResult(targets, startedAt, problems, logs) {
59780
+ writeResult(targets, {
59781
+ passed: problems.length === 0,
59782
+ seconds: (Date.now() - startedAt) / 1000,
59783
+ problems,
59784
+ suites: [],
59785
+ engines: [],
59786
+ logs,
59787
+ enginesAsked: targets.engines,
59788
+ slotsFree: 0,
59789
+ dealing: []
59790
+ });
59791
+ }
59792
+ function writeResult(targets, result) {
59793
+ writeJson(targets.lastRunFile, result);
59794
+ if (targets.resultFile !== null) {
59795
+ writeJson(targets.resultFile, result);
59593
59796
  }
59594
59797
  }
59595
59798
  function printUncollected(uncollected) {