gdharness 1.3.11 → 1.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +463 -256
- package/build/godot/tests/listed.gd +67 -2
- package/build/keeper.js +21 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|