gdharness 1.3.26 → 1.3.28
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
|
@@ -10915,6 +10915,7 @@ function readRun(printed) {
|
|
|
10915
10915
|
executed,
|
|
10916
10916
|
unloadable,
|
|
10917
10917
|
stoppedIn,
|
|
10918
|
+
running: last === null ? null : { suite: last.path, during },
|
|
10918
10919
|
leftOut,
|
|
10919
10920
|
engineVersion,
|
|
10920
10921
|
orphansLeft
|
|
@@ -11395,7 +11396,7 @@ async function inTurns(items, at, run) {
|
|
|
11395
11396
|
function heldToCores(cores, command, args, platform = process6.platform) {
|
|
11396
11397
|
return cores === null || platform !== "linux" ? { command, args: [...args] } : { command: "taskset", args: ["-c", `0-${String(cores - 1)}`, command, ...args] };
|
|
11397
11398
|
}
|
|
11398
|
-
async function watchedEngine(launch, args, directory, log, silenceSeconds) {
|
|
11399
|
+
async function watchedEngine(launch, args, directory, log, silenceSeconds, silenceIn) {
|
|
11399
11400
|
const user = join7(directory, "user");
|
|
11400
11401
|
const env = { ...process6.env, APPDATA: user, XDG_DATA_HOME: user };
|
|
11401
11402
|
const output = openSync2(log, "w");
|
|
@@ -11468,6 +11469,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds) {
|
|
|
11468
11469
|
const started = Date.now();
|
|
11469
11470
|
let lastGrowth = started;
|
|
11470
11471
|
let text = "";
|
|
11472
|
+
let allowed = { size: -1, seconds: silenceSeconds };
|
|
11471
11473
|
while (!finished.now) {
|
|
11472
11474
|
await delay2(LOOK_MS);
|
|
11473
11475
|
const deadline = launch.deadlineSeconds;
|
|
@@ -11488,9 +11490,18 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds) {
|
|
|
11488
11490
|
stop();
|
|
11489
11491
|
}
|
|
11490
11492
|
} else if (ended === null && Date.now() - lastGrowth > silenceSeconds * 1000) {
|
|
11491
|
-
|
|
11493
|
+
if (allowed.size !== size) {
|
|
11494
|
+
allowed = {
|
|
11495
|
+
size,
|
|
11496
|
+
seconds: silenceIn === undefined ? silenceSeconds : Math.max(silenceSeconds, silenceIn(readRun(text)))
|
|
11497
|
+
};
|
|
11498
|
+
}
|
|
11499
|
+
if (Date.now() - lastGrowth <= allowed.seconds * 1000) {
|
|
11500
|
+
continue;
|
|
11501
|
+
}
|
|
11502
|
+
const { stoppedIn } = readRun(text);
|
|
11492
11503
|
const unstarted = helper !== null && !existsSync5(helper.pid) ? `, and it never started ${helper.how}: ${helperStep(helper.step)}` : "";
|
|
11493
|
-
ended = `the engine printed nothing for ${String(
|
|
11504
|
+
ended = `the engine printed nothing for ${String(allowed.seconds)} s${stoppedIn === null ? "" : ` while running ${stoppedIn}`}, so it was ended${unstarted}`;
|
|
11494
11505
|
stop();
|
|
11495
11506
|
}
|
|
11496
11507
|
}
|
|
@@ -58366,7 +58377,23 @@ function getLocalVersion() {
|
|
|
58366
58377
|
|
|
58367
58378
|
// src/suite-run.ts
|
|
58368
58379
|
var SLOWEST_NAMED = 5;
|
|
58369
|
-
var
|
|
58380
|
+
var SILENCE_TIMES_TAKEN = 4;
|
|
58381
|
+
function silenceWhile(running, times, floorSeconds) {
|
|
58382
|
+
const time = running === null ? undefined : times[running.suite];
|
|
58383
|
+
if (running === null || time === undefined) {
|
|
58384
|
+
return floorSeconds;
|
|
58385
|
+
}
|
|
58386
|
+
let taken;
|
|
58387
|
+
if (running.during === null || running.during === "before") {
|
|
58388
|
+
const cases = Object.values(time.cases).reduce((sum, seconds) => sum + seconds, 0);
|
|
58389
|
+
taken = Math.max(0, time.seconds - cases);
|
|
58390
|
+
} else {
|
|
58391
|
+
const func = funcOf(running.during);
|
|
58392
|
+
const row = rowOf(running.during);
|
|
58393
|
+
taken = (row === null ? undefined : time.rows?.[func]?.[row]) ?? time.cases[func];
|
|
58394
|
+
}
|
|
58395
|
+
return Math.max(floorSeconds, SILENCE_TIMES_TAKEN * (taken ?? 0));
|
|
58396
|
+
}
|
|
58370
58397
|
var VALUED2 = new Set([
|
|
58371
58398
|
"--engines",
|
|
58372
58399
|
"--slots",
|
|
@@ -58660,12 +58687,12 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
58660
58687
|
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${String(SPLIT_PAST_SECONDS)} s last time: ${named(byRows)}.`);
|
|
58661
58688
|
}
|
|
58662
58689
|
const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
|
|
58663
|
-
let ran = (await inTurns(lanes, room.engines, (lane) => runLane(launch, lane,
|
|
58690
|
+
let ran = (await inTurns(lanes, room.engines, (lane) => runLane(launch, lane, times))).flat().sort((a, b) => a.index - b.index);
|
|
58664
58691
|
if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
|
|
58665
58692
|
console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
|
|
58666
58693
|
const again = [];
|
|
58667
58694
|
for (const engine of ran) {
|
|
58668
|
-
again.push(engine.verdict.passed ? engine : await runEngine2(launch, engine.plan, engine.index,
|
|
58695
|
+
again.push(engine.verdict.passed ? engine : await runEngine2(launch, engine.plan, engine.index, times, "again"));
|
|
58669
58696
|
}
|
|
58670
58697
|
ran = again;
|
|
58671
58698
|
}
|
|
@@ -58756,19 +58783,19 @@ async function importProject(launch) {
|
|
|
58756
58783
|
launch.slots.release(lock);
|
|
58757
58784
|
}
|
|
58758
58785
|
}
|
|
58759
|
-
async function runLane(launch, lane,
|
|
58786
|
+
async function runLane(launch, lane, times) {
|
|
58760
58787
|
const lock = await launch.slots.take();
|
|
58761
58788
|
try {
|
|
58762
58789
|
const ran = [];
|
|
58763
58790
|
for (const { plan, index } of lane) {
|
|
58764
|
-
ran.push(await runEngine2(launch, plan, index,
|
|
58791
|
+
ran.push(await runEngine2(launch, plan, index, times, "", lock));
|
|
58765
58792
|
}
|
|
58766
58793
|
return ran;
|
|
58767
58794
|
} finally {
|
|
58768
58795
|
launch.slots.release(lock);
|
|
58769
58796
|
}
|
|
58770
58797
|
}
|
|
58771
|
-
async function runEngine2(launch, plan, index,
|
|
58798
|
+
async function runEngine2(launch, plan, index, times, again = "", slot = null) {
|
|
58772
58799
|
const directory = join27(launch.runDirectory, `engine-${String(index)}${again === "" ? "" : `-${again}`}`);
|
|
58773
58800
|
mkdirSync10(join27(directory, "user"), { recursive: true });
|
|
58774
58801
|
const suitesFile = join27(directory, "suites.txt");
|
|
@@ -58816,15 +58843,12 @@ async function runEngine2(launch, plan, index, units, again = "", slot = null) {
|
|
|
58816
58843
|
"--expected-out",
|
|
58817
58844
|
expectedFile
|
|
58818
58845
|
];
|
|
58819
|
-
const held = new Set(plan.suites.map((suite) => suite.path));
|
|
58820
|
-
const slowest = Math.max(0, ...units.filter((unit) => held.has(unit.suite)).map((unit) => unit.cost));
|
|
58821
|
-
const silence = Math.max(launch.silenceFloorSeconds, SILENCE_TIMES_SLOWEST * slowest);
|
|
58822
58846
|
const log = join27(directory, "engine.log");
|
|
58823
58847
|
const lock = slot ?? await launch.slots.take();
|
|
58824
58848
|
const began = Date.now();
|
|
58825
58849
|
let end;
|
|
58826
58850
|
try {
|
|
58827
|
-
end = await watchedEngine(launch, args, directory, log,
|
|
58851
|
+
end = await watchedEngine(launch, args, directory, log, launch.silenceFloorSeconds, (printed) => silenceWhile(printed.running, times, launch.silenceFloorSeconds));
|
|
58828
58852
|
} finally {
|
|
58829
58853
|
if (slot === null) {
|
|
58830
58854
|
launch.slots.release(lock);
|
|
@@ -6,10 +6,27 @@ extends EditorPlugin
|
|
|
6
6
|
## that keeps showing the old file is how a change reads as one that never happened. The
|
|
7
7
|
## files are polled once a second; the editor's own watcher only fires on window focus.
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
## What was last seen of a watched file.
|
|
11
|
+
class GdhSeen:
|
|
12
|
+
## Its modification time, in whole seconds.
|
|
13
|
+
var time: int
|
|
14
|
+
## The MD5 of its contents.
|
|
15
|
+
var md5: String
|
|
16
|
+
|
|
17
|
+
func _init(seen_time: int, seen_md5: String) -> void:
|
|
18
|
+
time = seen_time
|
|
19
|
+
md5 = seen_md5
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
const CHECK_INTERVAL_SECONDS: float = 1.0
|
|
23
|
+
## How long after a file's modification time its contents are compared as well. The time is in
|
|
24
|
+
## whole seconds, so a second write inside the second of the first leaves it as it was and changes
|
|
25
|
+
## only the contents; past that second any write moves the time.
|
|
26
|
+
const SAME_SECOND_SECONDS: int = 5
|
|
10
27
|
|
|
11
28
|
var _timer: Timer
|
|
12
|
-
##
|
|
29
|
+
## What was last seen of each watched file, by path.
|
|
13
30
|
var _watched_files: Dictionary = {}
|
|
14
31
|
|
|
15
32
|
|
|
@@ -22,6 +39,11 @@ func _enter_tree() -> void:
|
|
|
22
39
|
var watching: int = _timer.timeout.connect(_check_for_changes)
|
|
23
40
|
if watching != OK:
|
|
24
41
|
push_error("gdharness: could not start watching for changes on disk")
|
|
42
|
+
# A scene's files are watched from the moment it opens: found at the next poll instead, a file
|
|
43
|
+
# written in between was first seen already changed, and its change was never reloaded.
|
|
44
|
+
var opening: int = scene_changed.connect(_on_scene_changed)
|
|
45
|
+
if opening != OK:
|
|
46
|
+
push_error("gdharness: could not start watching the scenes the editor opens")
|
|
25
47
|
add_child(_timer)
|
|
26
48
|
_timer.start()
|
|
27
49
|
_update_watched_files()
|
|
@@ -33,12 +55,16 @@ func _exit_tree() -> void:
|
|
|
33
55
|
_timer.queue_free()
|
|
34
56
|
|
|
35
57
|
|
|
58
|
+
func _on_scene_changed(_scene_root: Node) -> void:
|
|
59
|
+
_update_watched_files()
|
|
60
|
+
|
|
61
|
+
|
|
36
62
|
func _update_watched_files() -> void:
|
|
37
63
|
var edited_scene: Node = EditorInterface.get_edited_scene_root()
|
|
38
64
|
if edited_scene and edited_scene.scene_file_path:
|
|
39
65
|
var path: String = edited_scene.scene_file_path
|
|
40
66
|
if not _watched_files.has(path):
|
|
41
|
-
_watched_files[path] =
|
|
67
|
+
_watched_files[path] = _seen(path)
|
|
42
68
|
_watch_node_scripts(edited_scene)
|
|
43
69
|
|
|
44
70
|
|
|
@@ -48,11 +74,15 @@ func _watch_node_scripts(node: Node) -> void:
|
|
|
48
74
|
var attached: Script = script
|
|
49
75
|
var path: String = attached.resource_path
|
|
50
76
|
if not path.is_empty() and not _watched_files.has(path):
|
|
51
|
-
_watched_files[path] =
|
|
77
|
+
_watched_files[path] = _seen(path)
|
|
52
78
|
for child: Node in node.get_children():
|
|
53
79
|
_watch_node_scripts(child)
|
|
54
80
|
|
|
55
81
|
|
|
82
|
+
func _seen(path: String) -> GdhSeen:
|
|
83
|
+
return GdhSeen.new(_get_modified_time(path), _md5_of(path))
|
|
84
|
+
|
|
85
|
+
|
|
56
86
|
func _get_modified_time(path: String) -> int:
|
|
57
87
|
var global_path: String = ProjectSettings.globalize_path(path)
|
|
58
88
|
if FileAccess.file_exists(global_path):
|
|
@@ -60,20 +90,32 @@ func _get_modified_time(path: String) -> int:
|
|
|
60
90
|
return 0
|
|
61
91
|
|
|
62
92
|
|
|
93
|
+
func _md5_of(path: String) -> String:
|
|
94
|
+
var global_path: String = ProjectSettings.globalize_path(path)
|
|
95
|
+
if FileAccess.file_exists(global_path):
|
|
96
|
+
return FileAccess.get_md5(global_path)
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
|
|
63
100
|
func _check_for_changes() -> void:
|
|
64
101
|
_update_watched_files()
|
|
65
102
|
|
|
103
|
+
var now: int = int(Time.get_unix_time_from_system())
|
|
66
104
|
var scenes_to_reload: Array[String] = []
|
|
67
105
|
var scripts_to_reload: Array[String] = []
|
|
68
106
|
for path: String in _watched_files.keys():
|
|
107
|
+
var last: GdhSeen = _watched_files[path]
|
|
69
108
|
var current_time: int = _get_modified_time(path)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
if current_time == last.time and now - current_time >= SAME_SECOND_SECONDS:
|
|
110
|
+
continue
|
|
111
|
+
var current_md5: String = _md5_of(path)
|
|
112
|
+
_watched_files[path] = GdhSeen.new(current_time, current_md5)
|
|
113
|
+
if current_md5 == last.md5:
|
|
114
|
+
continue
|
|
115
|
+
if path.ends_with(".gd"):
|
|
116
|
+
scripts_to_reload.append(path)
|
|
117
|
+
else:
|
|
118
|
+
scenes_to_reload.append(path)
|
|
77
119
|
|
|
78
120
|
# Scripts first, so a scene reloaded after them instantiates the new code.
|
|
79
121
|
for path: String in scripts_to_reload:
|
|
@@ -85,16 +85,19 @@ static func button_event(
|
|
|
85
85
|
## [param answer], saying so when one of [param points] lies outside [param viewport]: the GUI picks
|
|
86
86
|
## what is under the pointer only inside it, so such a press or motion reaches no control, and its
|
|
87
87
|
## answer read the same as one that landed.
|
|
88
|
+
##
|
|
89
|
+
## The points are window pixels and the visible rect is canvas units, which differ by the window's
|
|
90
|
+
## content scale and any letterbox offset, so the rect is taken into window pixels first.
|
|
88
91
|
static func outside_noted(answer: Dictionary, viewport: Viewport, points: Array[Vector2]) -> Dictionary:
|
|
89
|
-
var shown: Rect2 = viewport.get_visible_rect()
|
|
92
|
+
var shown: Rect2 = viewport.get_final_transform() * viewport.get_visible_rect()
|
|
90
93
|
for point: Vector2 in points:
|
|
91
94
|
if not shown.has_point(point):
|
|
92
95
|
answer["outside_viewport"] = (
|
|
93
96
|
(
|
|
94
|
-
"%s is outside the window's viewport, %s, so no control there receives
|
|
97
|
+
"%s is outside the window's viewport, %s in window pixels, so no control there receives"
|
|
95
98
|
% [point, shown]
|
|
96
99
|
)
|
|
97
|
-
+ " _input and _unhandled_input still do."
|
|
100
|
+
+ " it; the game's _input and _unhandled_input still do."
|
|
98
101
|
)
|
|
99
102
|
break
|
|
100
103
|
return answer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.28",
|
|
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",
|