gdharness 1.3.10 → 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 +661 -294
- package/build/godot/tests/listed.gd +120 -2
- package/build/keeper.js +21 -4
- package/package.json +1 -1
|
@@ -15,6 +15,16 @@ extends SceneTree
|
|
|
15
15
|
|
|
16
16
|
const LISTED: String = "--suites-in"
|
|
17
17
|
const SKIPPING: String = "--skipping-in"
|
|
18
|
+
## The file of the parameterised cases dealt by their rows, one a line and tab-separated:
|
|
19
|
+
## `res://suite.gd:test_case`, then `only` and the rows this engine runs, or `besides` and the rows
|
|
20
|
+
## it leaves to the others, for the engine that takes every row dealt nowhere else. Apart from
|
|
21
|
+
## [constant SKIPPING], since gdUnit4 reads `suite:case:row` there as the case and leaves out every
|
|
22
|
+
## row of it.
|
|
23
|
+
const ROWS: String = "--rows-in"
|
|
24
|
+
## Printed before each row an engine leaves out, as `res://suite.gd > test_case:row`, -1 for a case
|
|
25
|
+
## that is not parameterised. gdUnit4 prints nothing for a case it skips, and the caller needs every
|
|
26
|
+
## row an engine found to tell one that no engine ran.
|
|
27
|
+
const LEFT_OUT: String = "gdharness left out"
|
|
18
28
|
## The flags turning off a report, with the gdUnit4 hook that writes it. A gate reads what a run
|
|
19
29
|
## prints, and each report finds every case it hears of in the session to write them all out as the
|
|
20
30
|
## run ends; project_test reads the JUnit one and turns off only the HTML one.
|
|
@@ -54,16 +64,33 @@ class GdhRunner:
|
|
|
54
64
|
extends GdUnitTestCIRunner
|
|
55
65
|
|
|
56
66
|
var own_path: String = ""
|
|
67
|
+
## The cases dealt by their rows, by `res://suite.gd:test_case`.
|
|
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
|
|
57
71
|
|
|
58
72
|
func get_cmdline_args() -> PackedStringArray:
|
|
59
73
|
return options_of(OS.get_cmdline_args(), own_path)
|
|
60
74
|
|
|
75
|
+
## gdUnit4 asks this of every case it finds, a parameterised case once a row, each row carrying
|
|
76
|
+
## its index.
|
|
77
|
+
func is_skipped(test: GdUnitTestCase) -> bool:
|
|
78
|
+
if super(test):
|
|
79
|
+
return true
|
|
80
|
+
var share: GdhRowShare = row_shares.get("%s:%s" % [test.suite_resource_path, test.test_name], null)
|
|
81
|
+
if share == null or not share.leaves_out(test.attribute_index):
|
|
82
|
+
return false
|
|
83
|
+
print("%s %s > %s:%d" % [LEFT_OUT, test.suite_resource_path, test.test_name, test.attribute_index])
|
|
84
|
+
return true
|
|
85
|
+
|
|
61
86
|
## The session gdUnit4 has just made, swapped for a [GdhIndexedSession] of the same cases as it
|
|
62
87
|
## is announced: everything that reads the session, the hooks, the cases and the console printing
|
|
63
88
|
## every case, is handed it after this.
|
|
64
89
|
func _on_gdunit_event(event: GdUnitEvent) -> void:
|
|
65
90
|
if event.type() == GdUnitEvent.SESSION_START:
|
|
66
91
|
_test_session = GdhIndexedSession.of(_test_session)
|
|
92
|
+
elif event.type() == GdUnitEvent.TESTCASE_AFTER:
|
|
93
|
+
cases_ended += 1
|
|
67
94
|
super(event)
|
|
68
95
|
|
|
69
96
|
## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
|
|
@@ -87,6 +114,19 @@ class GdhRunner:
|
|
|
87
114
|
return forward.to_lower() if OS.get_name() == "Windows" else forward
|
|
88
115
|
|
|
89
116
|
|
|
117
|
+
## The rows of one parameterised case an engine runs: those named, or every row but those.
|
|
118
|
+
class GdhRowShare:
|
|
119
|
+
extends RefCounted
|
|
120
|
+
|
|
121
|
+
var only: bool = true
|
|
122
|
+
var rows: PackedInt32Array = []
|
|
123
|
+
|
|
124
|
+
## Whether the engine leaves out [param row], which is -1 for a case that is not parameterised
|
|
125
|
+
## and so runs only in the engine taking the rows besides.
|
|
126
|
+
func leaves_out(row: int) -> bool:
|
|
127
|
+
return rows.has(row) != only
|
|
128
|
+
|
|
129
|
+
|
|
90
130
|
## gdUnit4's session finding a case by its id in a dictionary, where its own walks every case of the
|
|
91
131
|
## session, `GdUnitTestSession.find_test_by_id`: the console asks it twice a case and each report
|
|
92
132
|
## twice more, so a run's time in it grew with the square of its cases (17.9 s of processor time
|
|
@@ -94,6 +134,8 @@ class GdhRunner:
|
|
|
94
134
|
class GdhIndexedSession:
|
|
95
135
|
extends GdUnitTestSession
|
|
96
136
|
|
|
137
|
+
## The cases found by id, which [GdhSessionCheck] reads to tell that the console used this session.
|
|
138
|
+
var lookups: int = 0
|
|
97
139
|
var _by_id: Dictionary[String, GdUnitTestCase] = {}
|
|
98
140
|
|
|
99
141
|
func _init(cases: Array[GdUnitTestCase], reports_in: String) -> void:
|
|
@@ -102,6 +144,7 @@ class GdhIndexedSession:
|
|
|
102
144
|
_by_id[str(test.guid)] = test
|
|
103
145
|
|
|
104
146
|
func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
|
|
147
|
+
lookups += 1
|
|
105
148
|
var found: GdUnitTestCase = _by_id.get(str(id), null)
|
|
106
149
|
return found
|
|
107
150
|
|
|
@@ -113,6 +156,50 @@ class GdhIndexedSession:
|
|
|
113
156
|
return GdhIndexedSession.new(made._test_cases, made.report_path)
|
|
114
157
|
|
|
115
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
|
+
|
|
116
203
|
func _initialize() -> void:
|
|
117
204
|
OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
|
|
118
205
|
var asked: PackedStringArray = OS.get_cmdline_user_args()
|
|
@@ -120,8 +207,19 @@ func _initialize() -> void:
|
|
|
120
207
|
for flag: String in REPORTS_OFF:
|
|
121
208
|
if asked.has(flag):
|
|
122
209
|
var _unread: bool = unread.append(REPORTS_OFF[flag])
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
)
|
|
125
223
|
var suites: PackedStringArray = listed(asked, LISTED)
|
|
126
224
|
if asked.has(TOGETHER):
|
|
127
225
|
_together = compiled_together(suites)
|
|
@@ -132,6 +230,10 @@ func _initialize() -> void:
|
|
|
132
230
|
_runner.own_path = get_script().resource_path
|
|
133
231
|
_runner.add_test_suites(suites)
|
|
134
232
|
_runner.skip_test_suites(listed(asked, SKIPPING))
|
|
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))
|
|
135
237
|
root.add_child(_runner)
|
|
136
238
|
|
|
137
239
|
|
|
@@ -213,6 +315,22 @@ static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
|
|
|
213
315
|
return named
|
|
214
316
|
|
|
215
317
|
|
|
318
|
+
## The row shares [param lines] of a [constant ROWS] file give, by case.
|
|
319
|
+
static func row_shares_of(lines: PackedStringArray) -> Dictionary[String, GdhRowShare]:
|
|
320
|
+
var shares: Dictionary[String, GdhRowShare] = {}
|
|
321
|
+
for line: String in lines:
|
|
322
|
+
var parts: PackedStringArray = line.split("\t")
|
|
323
|
+
if parts.size() < 2:
|
|
324
|
+
continue
|
|
325
|
+
var share: GdhRowShare = GdhRowShare.new()
|
|
326
|
+
share.only = parts[1] == "only"
|
|
327
|
+
for row: String in parts.slice(2):
|
|
328
|
+
if row.is_valid_int():
|
|
329
|
+
var _added: bool = share.rows.append(row.to_int())
|
|
330
|
+
shares[parts[0]] = share
|
|
331
|
+
return shares
|
|
332
|
+
|
|
333
|
+
|
|
216
334
|
## The argument after [param flag] in [param args], or "" when there is none.
|
|
217
335
|
static func argument_after(args: PackedStringArray, flag: String) -> String:
|
|
218
336
|
var at: int = args.find(flag)
|
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",
|