gdharness 1.3.11 → 1.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,6 +66,8 @@ class GdhRunner:
66
66
  var own_path: String = ""
67
67
  ## The cases dealt by their rows, by `res://suite.gd:test_case`.
68
68
  var row_shares: Dictionary[String, GdhRowShare] = {}
69
+ ## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups against.
70
+ var cases_ended: int = 0
69
71
 
70
72
  func get_cmdline_args() -> PackedStringArray:
71
73
  return options_of(OS.get_cmdline_args(), own_path)
@@ -87,6 +89,8 @@ class GdhRunner:
87
89
  func _on_gdunit_event(event: GdUnitEvent) -> void:
88
90
  if event.type() == GdUnitEvent.SESSION_START:
89
91
  _test_session = GdhIndexedSession.of(_test_session)
92
+ elif event.type() == GdUnitEvent.TESTCASE_AFTER:
93
+ cases_ended += 1
90
94
  super(event)
91
95
 
92
96
  ## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
@@ -130,6 +134,8 @@ class GdhRowShare:
130
134
  class GdhIndexedSession:
131
135
  extends GdUnitTestSession
132
136
 
137
+ ## The cases found by id, which [GdhSessionCheck] reads to tell that the console used this session.
138
+ var lookups: int = 0
133
139
  var _by_id: Dictionary[String, GdUnitTestCase] = {}
134
140
 
135
141
  func _init(cases: Array[GdUnitTestCase], reports_in: String) -> void:
@@ -138,6 +144,7 @@ class GdhIndexedSession:
138
144
  _by_id[str(test.guid)] = test
139
145
 
140
146
  func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
147
+ lookups += 1
141
148
  var found: GdUnitTestCase = _by_id.get(str(id), null)
142
149
  return found
143
150
 
@@ -149,6 +156,50 @@ class GdhIndexedSession:
149
156
  return GdhIndexedSession.new(made._test_cases, made.report_path)
150
157
 
151
158
 
159
+ ## A session hook of the runner's own, failing the run when the [GdhIndexedSession] is not the
160
+ ## session gdUnit4 works from. The swap holds only while gdUnit4 announces a session before it hands
161
+ ## it to the hooks and the console, and a gdUnit4 that changed that order would otherwise run every
162
+ ## suite as before, only slower, with nothing said. Checked in the gdUnit4 the game installed, on
163
+ ## every run, rather than in the one gdharness tests against.
164
+ class GdhSessionCheck:
165
+ extends GdUnitTestSessionHook
166
+
167
+ var _runner: GdhRunner = null
168
+
169
+ func _init(runner: GdhRunner) -> void:
170
+ super("gdharness session check", "Fails the run when gdUnit4 works from another session.")
171
+ _runner = runner
172
+
173
+ func startup(session: GdUnitTestSession) -> GdUnitResult:
174
+ if session is GdhIndexedSession:
175
+ return GdUnitResult.success()
176
+ return GdUnitResult.error(
177
+ (
178
+ (
179
+ "gdharness test: gdUnit4 %s handed its session hooks the session before announcing it, so"
180
+ + " the runner's indexed session is not the one they read. This gdUnit4 starts a session"
181
+ + " differently from the ones gdharness knows; report it to gdharness with the version."
182
+ )
183
+ % GdUnit4Version.current()
184
+ )
185
+ )
186
+
187
+ func shutdown(session: GdUnitTestSession) -> GdUnitResult:
188
+ var indexed: GdhIndexedSession = session as GdhIndexedSession
189
+ if indexed == null or _runner.cases_ended == 0 or indexed.lookups > 0:
190
+ return GdUnitResult.success()
191
+ return GdUnitResult.error(
192
+ (
193
+ (
194
+ "gdharness test: gdUnit4 %s ran %d cases without looking one up in the runner's indexed"
195
+ + " session, so its console reads another session. This gdUnit4 starts a session"
196
+ + " differently from the ones gdharness knows; report it to gdharness with the version."
197
+ )
198
+ % [GdUnit4Version.current(), _runner.cases_ended]
199
+ )
200
+ )
201
+
202
+
152
203
  func _initialize() -> void:
153
204
  OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
154
205
  var asked: PackedStringArray = OS.get_cmdline_user_args()
@@ -156,8 +207,19 @@ func _initialize() -> void:
156
207
  for flag: String in REPORTS_OFF:
157
208
  if asked.has(flag):
158
209
  var _unread: bool = unread.append(REPORTS_OFF[flag])
159
- if not unread.is_empty():
160
- var _off: PackedStringArray = unread_off(GdUnitTestSessionHookService.instance(), unread)
210
+ var hooks: GdUnitTestSessionHookService = GdUnitTestSessionHookService.instance()
211
+ var off: PackedStringArray = unread_off(hooks, unread)
212
+ for path: String in unread:
213
+ if not off.has(path):
214
+ push_error(
215
+ (
216
+ (
217
+ "gdharness test: gdUnit4 %s registers no session hook %s, so the report it writes is not"
218
+ + " turned off; report it to gdharness with the version."
219
+ )
220
+ % [GdUnit4Version.current(), path]
221
+ )
222
+ )
161
223
  var suites: PackedStringArray = listed(asked, LISTED)
162
224
  if asked.has(TOGETHER):
163
225
  _together = compiled_together(suites)
@@ -169,6 +231,9 @@ func _initialize() -> void:
169
231
  _runner.add_test_suites(suites)
170
232
  _runner.skip_test_suites(listed(asked, SKIPPING))
171
233
  _runner.row_shares = row_shares_of(listed(asked, ROWS))
234
+ # Added to the list itself rather than through `register`, which saves every hook it holds into
235
+ # `project.godot`.
236
+ hooks.enigne_hooks.append(GdhSessionCheck.new(_runner))
172
237
  root.add_child(_runner)
173
238
 
174
239
 
package/build/keeper.js CHANGED
@@ -119,6 +119,9 @@ public static class GdharnessDesktop {
119
119
  [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
120
120
  [DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr handle, uint code);
121
121
  [DllImport("kernel32.dll")] static extern uint SetErrorMode(uint mode);
122
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetProcessAffinityMask(IntPtr process, UIntPtr mask);
123
+ [DllImport("kernel32.dll")] static extern uint ResumeThread(IntPtr thread);
124
+ const uint CreateSuspended = 0x4;
122
125
  const uint FailCriticalErrors = 0x1;
123
126
  const uint NoOpenFileErrorBox = 0x8000;
124
127
 
@@ -180,7 +183,8 @@ public static class GdharnessDesktop {
180
183
  public static int RunSpec(string specFile) {
181
184
  string[] lines = File.ReadAllLines(specFile);
182
185
  Func<int, string> field = delegate(int index) { return Encoding.UTF8.GetString(Convert.FromBase64String(lines[index])); };
183
- return Run(field(0), field(1), field(2), field(3) == "1", field(4), field(5), field(6));
186
+ string affinity = lines.Length > 7 ? field(7) : "";
187
+ return Run(field(0), field(1), field(2), field(3) == "1", field(4), field(5), field(6), affinity.Length == 0 ? 0UL : ulong.Parse(affinity));
184
188
  }
185
189
 
186
190
  static void Step(string stepFile, string step) {
@@ -189,7 +193,9 @@ public static class GdharnessDesktop {
189
193
  }
190
194
  }
191
195
 
192
- public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory, string stepFile) {
196
+ // With an affinity, the process starts suspended and is held to those processors before its
197
+ // first thread runs, so every thread it starts afterwards is held to them as well.
198
+ public static int Run(string desktop, string commandLine, string pidFile, bool noActivate, string stopFile, string directory, string stepFile, ulong affinity) {
193
199
  StartupInfo startup = new StartupInfo();
194
200
  startup.cb = Marshal.SizeOf(typeof(StartupInfo));
195
201
  if (desktop.Length > 0) {
@@ -219,9 +225,19 @@ public static class GdharnessDesktop {
219
225
  }
220
226
  try {
221
227
  Step(stepFile, "starting the process");
222
- if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, DetachedProcess, IntPtr.Zero, directory, ref startup, out started)) {
228
+ uint creation = DetachedProcess | (affinity == 0 ? 0u : CreateSuspended);
229
+ if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, true, creation, IntPtr.Zero, directory, ref startup, out started)) {
223
230
  throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW " + commandLine);
224
231
  }
232
+ if (affinity != 0) {
233
+ Step(stepFile, "holding the process to its processors");
234
+ if (!SetProcessAffinityMask(started.process, new UIntPtr(affinity))) {
235
+ int error = Marshal.GetLastWin32Error();
236
+ TerminateProcess(started.process, 1);
237
+ throw new Win32Exception(error, "SetProcessAffinityMask " + affinity);
238
+ }
239
+ ResumeThread(started.thread);
240
+ }
225
241
  File.WriteAllText(pidFile, started.processId.ToString());
226
242
  if (noActivate) {
227
243
  HoldTheForeground(started);
@@ -381,7 +397,8 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
381
397
  how.noActivate === true ? "1" : "0",
382
398
  files.stop,
383
399
  how.directory,
384
- files.step
400
+ files.step,
401
+ how.affinity === undefined ? "" : how.affinity.toString()
385
402
  ];
386
403
  writeFileSync(files.spec, fields.map((field) => Buffer.from(field, "utf8").toString("base64")).join(`
387
404
  `), "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
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",