gdharness 1.3.4 → 1.3.6
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 +2520 -936
- package/build/godot/addons/gdharness_editor/declared_values.gd +24 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +5 -18
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +48 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +16 -13
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +32 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
- package/build/godot/operations/classdb_queries.gd +112 -46
- package/build/godot/operations/declared_values.gd +24 -0
- package/build/godot/operations/gdscript_authoring.gd +15 -2
- package/build/godot/operations/property_values.gd +5 -18
- package/build/godot/tests/listed.gd +241 -0
- package/build/keeper.js +126 -76
- package/package.json +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
extends SceneTree
|
|
2
|
+
|
|
3
|
+
## gdUnit4's command-line runner over the suites a file lists, in the file's order. One `-a` a suite
|
|
4
|
+
## outgrows the command line once a game has a few hundred suites (Windows stops at 32767
|
|
5
|
+
## characters), so the suites come from the file named after `--` with [constant LISTED], and the
|
|
6
|
+
## cases to leave out from the one named with [constant SKIPPING]. Every other argument is gdUnit4's
|
|
7
|
+
## own, as `GdUnitCmdTool.gd` takes them.
|
|
8
|
+
##
|
|
9
|
+
## Started from outside the project, `-s` with this file's absolute path, so it declares no class of
|
|
10
|
+
## its own and reaches gdUnit4 through the project's global classes. Besides the file of suites it
|
|
11
|
+
## finds each case by its id in a dictionary ([GdhIndexedSession]), can compile the suites together
|
|
12
|
+
## before gdUnit4 loads them ([constant TOGETHER]), can turn off the reports the caller does
|
|
13
|
+
## not read ([constant REPORTS_OFF]), and writes the errors each suite declares it expects
|
|
14
|
+
## ([constant EXPECTED_OUT]).
|
|
15
|
+
|
|
16
|
+
const LISTED: String = "--suites-in"
|
|
17
|
+
const SKIPPING: String = "--skipping-in"
|
|
18
|
+
## The flags turning off a report, with the gdUnit4 hook that writes it. A gate reads what a run
|
|
19
|
+
## prints, and each report finds every case it hears of in the session to write them all out as the
|
|
20
|
+
## run ends; project_test reads the JUnit one and turns off only the HTML one.
|
|
21
|
+
const REPORTS_OFF: Dictionary[String, String] = {
|
|
22
|
+
"--html-report-off": "res://addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd",
|
|
23
|
+
"--junit-report-off": "res://addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd",
|
|
24
|
+
}
|
|
25
|
+
## Compiles the suites together beside every class of the game ([method compiled_together]) before
|
|
26
|
+
## gdUnit4 loads them. A gate asks for it, since it runs every suite and wants every script that
|
|
27
|
+
## does not compile named. project_test does not: its answer is about the scripts its run reached,
|
|
28
|
+
## and the compile names every broken script of the game, then once more as the one script it made.
|
|
29
|
+
const TOGETHER: String = "--compile-together"
|
|
30
|
+
## The file to write the errors every suite declares it expects into, as JSON keyed by suite path and
|
|
31
|
+
## then case name.
|
|
32
|
+
const EXPECTED_OUT: String = "--expected-errors-out"
|
|
33
|
+
## The constant a suite declares its expected errors in: case name to the exact texts it prints.
|
|
34
|
+
const EXPECTED: String = "GDHARNESS_EXPECTED_ERRORS"
|
|
35
|
+
## The tool name gdUnit4 reads its options after.
|
|
36
|
+
const TOOL: String = "GdUnitCmdTool.gd"
|
|
37
|
+
## How long a windowless engine sleeps after each frame. The engine's own default is the
|
|
38
|
+
## low-processor pause, 6.8 ms measured, which a run of suites awaiting frames spends thousands of
|
|
39
|
+
## times; zero makes a frame's delta round to nothing, and a tween then never moves.
|
|
40
|
+
const FRAME_SLEEP_USEC: int = 1000
|
|
41
|
+
|
|
42
|
+
var _runner: GdhRunner = null
|
|
43
|
+
## The suites of the run, compiled by [method compiled_together] and held for as long as it runs.
|
|
44
|
+
var _together: GDScript = null
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## gdUnit4's runner reading its options after this script's path rather than after its own tool's,
|
|
48
|
+
## which it looks for by name and otherwise finds none of them, printing its help.
|
|
49
|
+
class GdhRunner:
|
|
50
|
+
extends GdUnitTestCIRunner
|
|
51
|
+
|
|
52
|
+
var own_path: String = ""
|
|
53
|
+
|
|
54
|
+
func get_cmdline_args() -> PackedStringArray:
|
|
55
|
+
return options_of(OS.get_cmdline_args(), own_path)
|
|
56
|
+
|
|
57
|
+
## The session gdUnit4 has just made, swapped for a [GdhIndexedSession] of the same cases as it
|
|
58
|
+
## is announced: everything that reads the session, the hooks, the cases and the console printing
|
|
59
|
+
## every case, is handed it after this.
|
|
60
|
+
func _on_gdunit_event(event: GdUnitEvent) -> void:
|
|
61
|
+
if event.type() == GdUnitEvent.SESSION_START:
|
|
62
|
+
_test_session = GdhIndexedSession.of(_test_session)
|
|
63
|
+
super(event)
|
|
64
|
+
|
|
65
|
+
## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
|
|
66
|
+
## path is compared with its separators and, on Windows, its case folded, since the engine
|
|
67
|
+
## reports the script as `X:/a/listed.gd` when it was started as `X:\a\listed.gd`.
|
|
68
|
+
static func options_of(args: PackedStringArray, own: String) -> PackedStringArray:
|
|
69
|
+
var wanted: String = spelt(own)
|
|
70
|
+
var at: int = -1
|
|
71
|
+
for index: int in args.size():
|
|
72
|
+
if spelt(args[index]) == wanted:
|
|
73
|
+
at = index
|
|
74
|
+
break
|
|
75
|
+
if at < 0:
|
|
76
|
+
return args
|
|
77
|
+
var options: PackedStringArray = [TOOL]
|
|
78
|
+
options.append_array(args.slice(at + 1))
|
|
79
|
+
return options
|
|
80
|
+
|
|
81
|
+
static func spelt(path: String) -> String:
|
|
82
|
+
var forward: String = path.replace("\\", "/")
|
|
83
|
+
return forward.to_lower() if OS.get_name() == "Windows" else forward
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## gdUnit4's session finding a case by its id in a dictionary, where its own walks every case of the
|
|
87
|
+
## session, `GdUnitTestSession.find_test_by_id`: the console asks it twice a case and each report
|
|
88
|
+
## twice more, so a run's time in it grew with the square of its cases (17.9 s of processor time
|
|
89
|
+
## in an engine of 2300 cases).
|
|
90
|
+
class GdhIndexedSession:
|
|
91
|
+
extends GdUnitTestSession
|
|
92
|
+
|
|
93
|
+
var _by_id: Dictionary[String, GdUnitTestCase] = {}
|
|
94
|
+
|
|
95
|
+
func _init(cases: Array[GdUnitTestCase], reports_in: String) -> void:
|
|
96
|
+
super(cases, reports_in)
|
|
97
|
+
for test: GdUnitTestCase in _test_cases:
|
|
98
|
+
_by_id[str(test.guid)] = test
|
|
99
|
+
|
|
100
|
+
func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
|
|
101
|
+
var found: GdUnitTestCase = _by_id.get(str(id), null)
|
|
102
|
+
return found
|
|
103
|
+
|
|
104
|
+
## [param made] as an indexed session of its cases, reporting where it does; one already indexed
|
|
105
|
+
## as it is.
|
|
106
|
+
static func of(made: GdUnitTestSession) -> GdUnitTestSession:
|
|
107
|
+
if made is GdhIndexedSession:
|
|
108
|
+
return made
|
|
109
|
+
return GdhIndexedSession.new(made._test_cases, made.report_path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
func _initialize() -> void:
|
|
113
|
+
OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
|
|
114
|
+
var asked: PackedStringArray = OS.get_cmdline_user_args()
|
|
115
|
+
var unread: PackedStringArray = []
|
|
116
|
+
for flag: String in REPORTS_OFF:
|
|
117
|
+
if asked.has(flag):
|
|
118
|
+
var _unread: bool = unread.append(REPORTS_OFF[flag])
|
|
119
|
+
if not unread.is_empty():
|
|
120
|
+
var _off: PackedStringArray = unread_off(GdUnitTestSessionHookService.instance(), unread)
|
|
121
|
+
var suites: PackedStringArray = listed(asked, LISTED)
|
|
122
|
+
if asked.has(TOGETHER):
|
|
123
|
+
_together = compiled_together(suites)
|
|
124
|
+
var expected_out: String = argument_after(asked, EXPECTED_OUT)
|
|
125
|
+
if not expected_out.is_empty():
|
|
126
|
+
write_expected(expected_out, suites)
|
|
127
|
+
_runner = GdhRunner.new()
|
|
128
|
+
_runner.own_path = get_script().resource_path
|
|
129
|
+
_runner.add_test_suites(suites)
|
|
130
|
+
_runner.skip_test_suites(listed(asked, SKIPPING))
|
|
131
|
+
root.add_child(_runner)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
## As gdUnit4's own `GdUnitCmdTool.gd` ends, which names in a verbose run the nodes left orphaned.
|
|
135
|
+
func _finalize() -> void:
|
|
136
|
+
queue_delete(_runner)
|
|
137
|
+
if OS.is_stdout_verbose():
|
|
138
|
+
print("Orphan nodes left as the run ends:")
|
|
139
|
+
Window.print_orphan_nodes()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## Turns off the hooks among [param hooks] whose scripts [param unread] names, answering the scripts
|
|
143
|
+
## of those it turned off. Through the mark gdUnit4's own `is_enabled` reads, rather than its
|
|
144
|
+
## `enable_hook`, which writes the setting into `project.godot`: a run changes nothing in the tree.
|
|
145
|
+
static func unread_off(hooks: GdUnitTestSessionHookService, unread: PackedStringArray) -> PackedStringArray:
|
|
146
|
+
var off: PackedStringArray = []
|
|
147
|
+
for hook: GdUnitTestSessionHook in hooks.enigne_hooks:
|
|
148
|
+
var path: String = hook.get_script().resource_path
|
|
149
|
+
if unread.has(path):
|
|
150
|
+
hook.set_meta("enabled", false)
|
|
151
|
+
var _added: bool = off.append(path)
|
|
152
|
+
return off
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
## [param suites] compiled inside one script, answered for the caller to hold so gdUnit4's own loads
|
|
156
|
+
## find them compiled. The engine keeps what it read of a class only while the compile that asked for
|
|
157
|
+
## it runs (`GDScriptCache`), so a suite compiled on its own reads again every class it reaches a
|
|
158
|
+
## member of, some seventy milliseconds a suite. As the preloads of one script naming every class of
|
|
159
|
+
## the game's own first, held from that script's compile, they share one reading of each: two hundred
|
|
160
|
+
## suites compiled in a third of the time they took one at a time. The names are public, since a
|
|
161
|
+
## private one unread is a warning and every warning an error, and each a variable, since a class is
|
|
162
|
+
## no constant expression. A suite that does not compile fails here and again when gdUnit4 loads it,
|
|
163
|
+
## which reports it as it would have. A directory listed is left to gdUnit4 to walk.
|
|
164
|
+
static func compiled_together(suites: PackedStringArray) -> GDScript:
|
|
165
|
+
var lines: PackedStringArray = ["extends RefCounted"]
|
|
166
|
+
var at: int = 0
|
|
167
|
+
for named: String in held_classes():
|
|
168
|
+
var _held: bool = lines.append("var held_%d: Variant = %s" % [at, named])
|
|
169
|
+
at += 1
|
|
170
|
+
at = 0
|
|
171
|
+
for path: String in suites:
|
|
172
|
+
if not path.ends_with(".gd"):
|
|
173
|
+
continue
|
|
174
|
+
var _suite: bool = lines.append('var suite_%d: Variant = preload("%s")' % [at, path])
|
|
175
|
+
at += 1
|
|
176
|
+
var together: GDScript = GDScript.new()
|
|
177
|
+
together.source_code = "\n".join(lines) + "\n"
|
|
178
|
+
var _compiled: Error = together.reload()
|
|
179
|
+
return together
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## The classes [method compiled_together] names before the suites: the game's own GDScript classes,
|
|
183
|
+
## none of an addon's or a suite's, then gdUnit4's suite. Only GDScript shares the engine's reading,
|
|
184
|
+
## and a sandboxed mod's SafeGDScript class would load the sandbox's compiler, some 290 MB and up to
|
|
185
|
+
## 2.5 s, for nothing.
|
|
186
|
+
static func held_classes(
|
|
187
|
+
classes: Array[Dictionary] = ProjectSettings.get_global_class_list()
|
|
188
|
+
) -> PackedStringArray:
|
|
189
|
+
var held: PackedStringArray = []
|
|
190
|
+
for known: Dictionary in classes:
|
|
191
|
+
var path: String = known["path"]
|
|
192
|
+
var own: bool = not path.begins_with("res://addons/") and not path.ends_with("_test.gd")
|
|
193
|
+
if own and known["language"] == &"GDScript":
|
|
194
|
+
var _own: bool = held.append(str(known["class"]))
|
|
195
|
+
var _suite: bool = held.append("GdUnitTestSuite")
|
|
196
|
+
return held
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
## What the file after [param flag] in [param args] names, one a line in order, or none when no file
|
|
200
|
+
## is named: gdUnit4 then says it found nothing, which a gate refuses.
|
|
201
|
+
static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
|
|
202
|
+
var path: String = argument_after(args, flag)
|
|
203
|
+
if path.is_empty():
|
|
204
|
+
return PackedStringArray()
|
|
205
|
+
var named: PackedStringArray = []
|
|
206
|
+
for line: String in FileAccess.get_file_as_string(path).split("\n", false):
|
|
207
|
+
if not line.strip_edges().is_empty():
|
|
208
|
+
var _named: bool = named.append(line.strip_edges())
|
|
209
|
+
return named
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## The argument after [param flag] in [param args], or "" when there is none.
|
|
213
|
+
static func argument_after(args: PackedStringArray, flag: String) -> String:
|
|
214
|
+
var at: int = args.find(flag)
|
|
215
|
+
if at < 0 or at + 1 >= args.size():
|
|
216
|
+
return ""
|
|
217
|
+
return args[at + 1]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## The errors each of [param suites] declares it expects, by suite path and then case name, read off
|
|
221
|
+
## the compiled suite's constants so a declaration is whatever the engine made of it. A suite
|
|
222
|
+
## declaring none is left out.
|
|
223
|
+
static func expected_by_suite(suites: PackedStringArray) -> Dictionary:
|
|
224
|
+
var expected: Dictionary = {}
|
|
225
|
+
for path: String in suites:
|
|
226
|
+
if not path.ends_with(".gd") or not ResourceLoader.exists(path):
|
|
227
|
+
continue
|
|
228
|
+
var suite: Script = load(path)
|
|
229
|
+
var declared: Variant = suite.get_script_constant_map().get(EXPECTED)
|
|
230
|
+
if declared is Dictionary:
|
|
231
|
+
expected[path] = declared
|
|
232
|
+
return expected
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
static func write_expected(path: String, suites: PackedStringArray) -> void:
|
|
236
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
237
|
+
if file == null:
|
|
238
|
+
push_error("Could not write the expected errors to %s: %s" % [path, FileAccess.get_open_error()])
|
|
239
|
+
return
|
|
240
|
+
var _stored: bool = file.store_string(JSON.stringify(expected_by_suite(suites)))
|
|
241
|
+
file.close()
|
package/build/keeper.js
CHANGED
|
@@ -3,9 +3,33 @@
|
|
|
3
3
|
// src/keeper.ts
|
|
4
4
|
import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync3, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
5
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
6
|
-
import { join as
|
|
6
|
+
import { join as join7 } from "node:path";
|
|
7
7
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
8
8
|
|
|
9
|
+
// src/alive.ts
|
|
10
|
+
import process2 from "node:process";
|
|
11
|
+
function exitOf(child) {
|
|
12
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
13
|
+
return Promise.resolve({ code: child.exitCode, signal: child.signalCode });
|
|
14
|
+
}
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
child.once("exit", (code, signal) => {
|
|
17
|
+
resolve({ code, signal });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function alive(pid) {
|
|
22
|
+
if (pid === undefined || pid === null) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
process2.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return error.code === "EPERM";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
// src/desktop.ts
|
|
10
34
|
import { createHash } from "node:crypto";
|
|
11
35
|
import {
|
|
@@ -23,20 +47,6 @@ import { tmpdir } from "node:os";
|
|
|
23
47
|
import { join } from "node:path";
|
|
24
48
|
import { setTimeout as delay } from "node:timers/promises";
|
|
25
49
|
|
|
26
|
-
// src/alive.ts
|
|
27
|
-
import process2 from "node:process";
|
|
28
|
-
function alive(pid) {
|
|
29
|
-
if (pid === undefined || pid === null) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
process2.kill(pid, 0);
|
|
34
|
-
return true;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
return error.code === "EPERM";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
50
|
// src/spawn.ts
|
|
41
51
|
import {
|
|
42
52
|
execFile,
|
|
@@ -96,6 +106,9 @@ public static class GdharnessDesktop {
|
|
|
96
106
|
[DllImport("kernel32.dll")] static extern bool GetExitCodeProcess(IntPtr handle, out uint code);
|
|
97
107
|
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
98
108
|
[DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr handle, uint code);
|
|
109
|
+
[DllImport("kernel32.dll")] static extern uint SetErrorMode(uint mode);
|
|
110
|
+
const uint FailCriticalErrors = 0x1;
|
|
111
|
+
const uint NoOpenFileErrorBox = 0x8000;
|
|
99
112
|
|
|
100
113
|
delegate bool EachWindow(IntPtr window, IntPtr unused);
|
|
101
114
|
[DllImport("user32.dll")] static extern bool EnumWindows(EachWindow each, IntPtr unused);
|
|
@@ -169,6 +182,11 @@ public static class GdharnessDesktop {
|
|
|
169
182
|
startup.output = GetStdHandle(-11);
|
|
170
183
|
startup.error = startup.output;
|
|
171
184
|
Started started;
|
|
185
|
+
// Inherited by what it starts: without SEM_NOGPFAULTERRORBOX, Windows Error Reporting runs when
|
|
186
|
+
// the target crashes and writes its dump, and with the other two no hard-error box can open.
|
|
187
|
+
// Set outright rather than cleared from this process's own: Windows PowerShell changes the mode
|
|
188
|
+
// it inherited as it starts, and was seen reading 0x1 here, without the open-file bit.
|
|
189
|
+
SetErrorMode(FailCriticalErrors | NoOpenFileErrorBox);
|
|
172
190
|
if (noActivate) {
|
|
173
191
|
LockSetForegroundWindow(LockForeground);
|
|
174
192
|
}
|
|
@@ -350,6 +368,33 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
|
|
|
350
368
|
};
|
|
351
369
|
}
|
|
352
370
|
|
|
371
|
+
// src/error-mode.ts
|
|
372
|
+
import process3 from "node:process";
|
|
373
|
+
var NO_FAULT_ERROR_BOX = 2;
|
|
374
|
+
var cleared = process3.platform !== "win32";
|
|
375
|
+
async function letWindowsReportCrashes() {
|
|
376
|
+
if (cleared || !("bun" in process3.versions)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const { dlopen, FFIType } = await import("bun:ffi");
|
|
381
|
+
const kernel32 = dlopen("kernel32.dll", {
|
|
382
|
+
GetErrorMode: { args: [], returns: FFIType.u32 },
|
|
383
|
+
SetErrorMode: { args: [FFIType.u32], returns: FFIType.u32 }
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
const mode = kernel32.symbols.GetErrorMode();
|
|
387
|
+
kernel32.symbols.SetErrorMode(mode & ~NO_FAULT_ERROR_BOX);
|
|
388
|
+
cleared = true;
|
|
389
|
+
} finally {
|
|
390
|
+
kernel32.close();
|
|
391
|
+
}
|
|
392
|
+
} catch {}
|
|
393
|
+
}
|
|
394
|
+
function crashesReported() {
|
|
395
|
+
return cleared;
|
|
396
|
+
}
|
|
397
|
+
|
|
353
398
|
// src/game-debugger.ts
|
|
354
399
|
import { createServer } from "node:net";
|
|
355
400
|
|
|
@@ -1031,7 +1076,7 @@ class GameDebugger {
|
|
|
1031
1076
|
}
|
|
1032
1077
|
if (!held.hasStack) {
|
|
1033
1078
|
this.forget("debug_enter");
|
|
1034
|
-
this.send("
|
|
1079
|
+
this.send("step", [], held.thread);
|
|
1035
1080
|
const again = await this.next(GameDebugger.named("debug_enter"), STEP_MS);
|
|
1036
1081
|
if (again === null) {
|
|
1037
1082
|
return {
|
|
@@ -1132,7 +1177,7 @@ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "node:fs";
|
|
|
1132
1177
|
import { connect, createServer as createServer2 } from "node:net";
|
|
1133
1178
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
1134
1179
|
import { join as join2 } from "node:path";
|
|
1135
|
-
import
|
|
1180
|
+
import process4 from "node:process";
|
|
1136
1181
|
var DEBUG_PREFIX = "debug ";
|
|
1137
1182
|
function endedOrWhyNot(end) {
|
|
1138
1183
|
try {
|
|
@@ -1159,7 +1204,7 @@ function errorText(error) {
|
|
|
1159
1204
|
async function listenForTheServer(end, debug) {
|
|
1160
1205
|
let directory = null;
|
|
1161
1206
|
try {
|
|
1162
|
-
directory =
|
|
1207
|
+
directory = process4.platform === "win32" ? null : mkdtempSync2(join2(tmpdir2(), "gdharness-keeper-"));
|
|
1163
1208
|
const address = directory === null ? `\\\\.\\pipe\\gdharness-keeper-${randomUUID()}` : join2(directory, "keeper.sock");
|
|
1164
1209
|
const server = createServer2((socket) => {
|
|
1165
1210
|
socket.setEncoding("utf8");
|
|
@@ -1253,8 +1298,28 @@ function readLaunched(printed) {
|
|
|
1253
1298
|
|
|
1254
1299
|
// src/run-record.ts
|
|
1255
1300
|
import { createHash as createHash2 } from "node:crypto";
|
|
1256
|
-
import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as readFileSync2,
|
|
1257
|
-
import { basename
|
|
1301
|
+
import { mkdirSync, openSync as openSync2, readdirSync, readFileSync as readFileSync2, rmSync as rmSync4, statSync as statSync3 } from "node:fs";
|
|
1302
|
+
import { basename as basename2, join as join5, resolve as resolve3 } from "node:path";
|
|
1303
|
+
|
|
1304
|
+
// src/paths.ts
|
|
1305
|
+
import { realpathSync, statSync as statSync2 } from "node:fs";
|
|
1306
|
+
import { basename, dirname, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
|
|
1307
|
+
function realAsFarAsItExists(path) {
|
|
1308
|
+
const missing = [];
|
|
1309
|
+
let at = path;
|
|
1310
|
+
for (;; ) {
|
|
1311
|
+
try {
|
|
1312
|
+
return join3(realpathSync.native(at), ...missing);
|
|
1313
|
+
} catch {
|
|
1314
|
+
const parent = dirname(at);
|
|
1315
|
+
if (parent === at) {
|
|
1316
|
+
return path;
|
|
1317
|
+
}
|
|
1318
|
+
missing.unshift(basename(at));
|
|
1319
|
+
at = parent;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1258
1323
|
|
|
1259
1324
|
// src/windows-ask.ts
|
|
1260
1325
|
var SCRIPT = [
|
|
@@ -1278,7 +1343,7 @@ var SCRIPT = [
|
|
|
1278
1343
|
|
|
1279
1344
|
// src/runtime-client.ts
|
|
1280
1345
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
1281
|
-
import { dirname, join as
|
|
1346
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "node:path";
|
|
1282
1347
|
|
|
1283
1348
|
// src/launch.ts
|
|
1284
1349
|
function envValue(name, variables = process.env) {
|
|
@@ -1323,7 +1388,7 @@ function runtimeDirectory(variables = process.env) {
|
|
|
1323
1388
|
return explicit;
|
|
1324
1389
|
}
|
|
1325
1390
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
1326
|
-
return
|
|
1391
|
+
return join4(perUser ?? tmpdir3(), "gdharness");
|
|
1327
1392
|
}
|
|
1328
1393
|
var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
|
|
1329
1394
|
var verdicts = new Map;
|
|
@@ -1363,31 +1428,15 @@ function writeWhole(path, text) {
|
|
|
1363
1428
|
|
|
1364
1429
|
// src/run-record.ts
|
|
1365
1430
|
function runsDirectory() {
|
|
1366
|
-
return
|
|
1367
|
-
}
|
|
1368
|
-
function realAsFarAsItExists(path) {
|
|
1369
|
-
const missing = [];
|
|
1370
|
-
let at = path;
|
|
1371
|
-
for (;; ) {
|
|
1372
|
-
try {
|
|
1373
|
-
return join4(realpathSync.native(at), ...missing);
|
|
1374
|
-
} catch {
|
|
1375
|
-
const parent = dirname2(at);
|
|
1376
|
-
if (parent === at) {
|
|
1377
|
-
return path;
|
|
1378
|
-
}
|
|
1379
|
-
missing.unshift(basename(at));
|
|
1380
|
-
at = parent;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1431
|
+
return join5(runtimeDirectory(), "runs");
|
|
1383
1432
|
}
|
|
1384
1433
|
function projectKey(projectPath) {
|
|
1385
|
-
const real = realAsFarAsItExists(
|
|
1434
|
+
const real = realAsFarAsItExists(resolve3(projectPath)).replace(/[\\/]+$/, "");
|
|
1386
1435
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
1387
1436
|
return createHash2("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
1388
1437
|
}
|
|
1389
1438
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
1390
|
-
return
|
|
1439
|
+
return join5(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
1391
1440
|
}
|
|
1392
1441
|
function writeRunRecord(record) {
|
|
1393
1442
|
mkdirSync(runsDirectory(), { recursive: true });
|
|
@@ -1454,14 +1503,14 @@ function recordAt(path) {
|
|
|
1454
1503
|
// src/scratch.ts
|
|
1455
1504
|
import { existsSync as existsSync2, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync5 } from "node:fs";
|
|
1456
1505
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
1457
|
-
import { join as
|
|
1458
|
-
import
|
|
1506
|
+
import { join as join6 } from "node:path";
|
|
1507
|
+
import process5 from "node:process";
|
|
1459
1508
|
var SWEPT = { recursive: true, force: true };
|
|
1460
1509
|
var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
|
|
1461
1510
|
var waiting = new Set;
|
|
1462
1511
|
var atExit = new Set;
|
|
1463
1512
|
function scratchDirectory(kind) {
|
|
1464
|
-
return mkdtempSync3(
|
|
1513
|
+
return mkdtempSync3(join6(tmpdir4(), `gdharness-${kind}-${process5.pid}-`));
|
|
1465
1514
|
}
|
|
1466
1515
|
function discardWith(remove, paths) {
|
|
1467
1516
|
const left = [];
|
|
@@ -1519,7 +1568,7 @@ function lastTryOnExit() {
|
|
|
1519
1568
|
return;
|
|
1520
1569
|
}
|
|
1521
1570
|
lastTryWatched = true;
|
|
1522
|
-
|
|
1571
|
+
process5.once("exit", () => {
|
|
1523
1572
|
for (const path of [...waiting, ...atExit]) {
|
|
1524
1573
|
try {
|
|
1525
1574
|
remove(path);
|
|
@@ -1921,16 +1970,16 @@ async function startDirectly(spec, output, hidden, detached = true, errors = out
|
|
|
1921
1970
|
}
|
|
1922
1971
|
async function startThroughHelper(spec, output, stopFile) {
|
|
1923
1972
|
const scratch = scratchDirectory("desktop");
|
|
1924
|
-
const pidFile =
|
|
1925
|
-
const errorFile =
|
|
1926
|
-
const where = spec.desktop
|
|
1973
|
+
const pidFile = join7(scratch, "pid");
|
|
1974
|
+
const errorFile = join7(scratch, "error");
|
|
1975
|
+
const where = spec.desktop !== undefined ? `on the ${spec.desktop} desktop` : spec.noActivate === true ? "without activation" : "through the desktop helper";
|
|
1927
1976
|
try {
|
|
1928
1977
|
const compiled = await ensureHelperCompiled();
|
|
1929
1978
|
if (compiled !== null) {
|
|
1930
1979
|
return { error: `it could not be started ${where}: ${compiled}` };
|
|
1931
1980
|
}
|
|
1932
1981
|
const wrapper = throughHelper({ ...spec, directory: process.cwd() }, spec.command, spec.args, {
|
|
1933
|
-
spec:
|
|
1982
|
+
spec: join7(scratch, "spec.json"),
|
|
1934
1983
|
pid: pidFile,
|
|
1935
1984
|
error: errorFile,
|
|
1936
1985
|
stop: stopFile
|
|
@@ -1974,7 +2023,7 @@ function profiledBy(path, projectPath) {
|
|
|
1974
2023
|
};
|
|
1975
2024
|
let projectGodot = null;
|
|
1976
2025
|
try {
|
|
1977
|
-
projectGodot = readFileSync3(
|
|
2026
|
+
projectGodot = readFileSync3(join7(projectPath, "project.godot"), "utf8");
|
|
1978
2027
|
} catch {}
|
|
1979
2028
|
const observer = new ProfileObserver((totals) => {
|
|
1980
2029
|
const first = latest === null;
|
|
@@ -2006,10 +2055,10 @@ async function startDetached(spec, output, hidden, stopFile = "") {
|
|
|
2006
2055
|
return throughTheHelper(spec) ? await startThroughHelper(spec, output, stopFile) : await startDirectly(spec, output, hidden);
|
|
2007
2056
|
}
|
|
2008
2057
|
function throughTheHelper(spec) {
|
|
2009
|
-
return process.platform === "win32" && (
|
|
2058
|
+
return needsItsHelperKept(spec) || process.platform === "win32" && !crashesReported();
|
|
2010
2059
|
}
|
|
2011
2060
|
function needsItsHelperKept(spec) {
|
|
2012
|
-
return
|
|
2061
|
+
return process.platform === "win32" && (spec.desktop !== undefined || spec.noActivate === true);
|
|
2013
2062
|
}
|
|
2014
2063
|
async function launch() {
|
|
2015
2064
|
const spec = await receivedSpec();
|
|
@@ -2077,8 +2126,8 @@ async function keep() {
|
|
|
2077
2126
|
const debuggerPeer = run === undefined || run.debugged === false ? null : await GameDebugger.open(run.breakpoints ?? [], profiled === null ? [] : [profiled.observer]);
|
|
2078
2127
|
const toStart = debuggerPeer === null ? spec : { ...spec, args: withEngineArguments(spec.args, debuggerPeer.engineArguments) };
|
|
2079
2128
|
const transcript = run === undefined ? "ignore" : openSync3(run.transcript, "a");
|
|
2080
|
-
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(
|
|
2081
|
-
const stopFile = own === null ? "" :
|
|
2129
|
+
const own = run !== undefined && throughTheHelper(spec) ? mkdtempSync4(join7(tmpdir5(), "gdharness-stop-")) : null;
|
|
2130
|
+
const stopFile = own === null ? "" : join7(own, "stop");
|
|
2082
2131
|
let started;
|
|
2083
2132
|
try {
|
|
2084
2133
|
started = await startDetached(toStart, transcript, run === undefined, stopFile);
|
|
@@ -2108,26 +2157,8 @@ async function keep() {
|
|
|
2108
2157
|
if (record !== null) {
|
|
2109
2158
|
writeNoteIfItCan(record);
|
|
2110
2159
|
}
|
|
2160
|
+
const exited = exitOf(child);
|
|
2111
2161
|
let listening = null;
|
|
2112
|
-
child.once("exit", (code, signal) => {
|
|
2113
|
-
const finish = () => {
|
|
2114
|
-
profiled?.finish();
|
|
2115
|
-
if (run !== undefined) {
|
|
2116
|
-
recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
|
|
2117
|
-
}
|
|
2118
|
-
debuggerPeer?.close();
|
|
2119
|
-
listening?.close();
|
|
2120
|
-
if (own !== null) {
|
|
2121
|
-
rmSync6(own, { recursive: true, force: true });
|
|
2122
|
-
}
|
|
2123
|
-
process.exit(0);
|
|
2124
|
-
};
|
|
2125
|
-
if (debuggerPeer === null) {
|
|
2126
|
-
finish();
|
|
2127
|
-
} else {
|
|
2128
|
-
debuggerPeer.drained(DRAIN_MS).then(finish);
|
|
2129
|
-
}
|
|
2130
|
-
});
|
|
2131
2162
|
if (record !== null) {
|
|
2132
2163
|
listening = await listenForTheServer(() => {
|
|
2133
2164
|
if (stopFile !== "") {
|
|
@@ -2140,10 +2171,29 @@ async function keep() {
|
|
|
2140
2171
|
writeNoteIfItCan({ ...record, keeper: listening.address });
|
|
2141
2172
|
}
|
|
2142
2173
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2174
|
+
await new Promise((resolve) => {
|
|
2175
|
+
process.stdout.once("error", () => {
|
|
2176
|
+
resolve();
|
|
2177
|
+
});
|
|
2178
|
+
process.stdout.end(`pid ${pid}
|
|
2179
|
+
`, resolve);
|
|
2180
|
+
});
|
|
2181
|
+
const { code, signal } = await exited;
|
|
2182
|
+
if (debuggerPeer !== null) {
|
|
2183
|
+
await debuggerPeer.drained(DRAIN_MS);
|
|
2184
|
+
}
|
|
2185
|
+
profiled?.finish();
|
|
2186
|
+
if (run !== undefined) {
|
|
2187
|
+
recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
|
|
2188
|
+
}
|
|
2189
|
+
debuggerPeer?.close();
|
|
2190
|
+
listening?.close();
|
|
2191
|
+
if (own !== null) {
|
|
2192
|
+
rmSync6(own, { recursive: true, force: true });
|
|
2193
|
+
}
|
|
2194
|
+
process.exit(0);
|
|
2146
2195
|
}
|
|
2196
|
+
await letWindowsReportCrashes();
|
|
2147
2197
|
var mode = process.argv[2];
|
|
2148
2198
|
if (mode === "launch") {
|
|
2149
2199
|
await launch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
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",
|