gdharness 1.3.11 → 1.3.13
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 +19152 -17965
- package/build/godot/addons/gdharness_editor/bridge_client.gd +49 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd +7 -4
- package/build/godot/addons/gdharness_editor/serialisation.gd +2 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +39 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +19 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +2 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +39 -0
- package/build/godot/addons/gdharness_tests/bench.gd +28 -0
- package/build/godot/addons/gdharness_tests/bench.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/event.gd +92 -0
- package/build/godot/addons/gdharness_tests/event.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/share.gd +289 -0
- package/build/godot/addons/gdharness_tests/share.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/suites.gd +34 -0
- package/build/godot/addons/gdharness_tests/suites.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/suites.tscn +6 -0
- package/build/godot/addons/gdharness_tests/tweens.gd +24 -0
- package/build/godot/addons/gdharness_tests/tweens.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/wait.gd +53 -0
- package/build/godot/addons/gdharness_tests/wait.gd.uid +1 -0
- package/build/godot/loader/loader.gd +28 -0
- package/build/godot/operations/class_cache.gd +1 -1
- package/build/godot/operations/file_walk.gd +7 -4
- package/build/godot/operations/input_actions.gd +2 -2
- package/build/godot/operations/plugins.gd +2 -2
- package/build/godot/operations/project_config.gd +37 -10
- package/build/godot/operations/project_file.gd +89 -31
- package/build/godot/operations/serialisation.gd +2 -0
- package/build/godot/operations/value_shapes.gd +39 -0
- package/build/godot/tests/listed.gd +196 -20
- package/build/keeper.js +8398 -911
- package/package.json +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
class_name GdhShare
|
|
2
|
+
extends RefCounted
|
|
3
|
+
## Plays a bench's runs here, or this engine's share of them under `gdharness spread`, and hands
|
|
4
|
+
## back every run's outcome in run order.
|
|
5
|
+
##
|
|
6
|
+
## A bench whose run `i` depends on `i` alone (its seed worked out from the index, nothing carried
|
|
7
|
+
## from one run to the next) calls [method outcomes] for each arm with the number of runs and a
|
|
8
|
+
## callable playing run `i`. Run on its own, the scene plays every run here, as it always did. Under
|
|
9
|
+
## `gdharness spread`, each worker engine plays every Nth run from its own index and leaves what each
|
|
10
|
+
## run answered; then one more engine runs the scene to gather, and there [method outcomes] plays
|
|
11
|
+
## nothing and answers every run's outcome, read back as the type it went in as. The bench folds them
|
|
12
|
+
## in run order either way, so a spread table is the same table to the last digit, floats included.
|
|
13
|
+
##
|
|
14
|
+
## An outcome is a plain value: a number, a string, a vector, or an array or dictionary of them. An
|
|
15
|
+
## object cannot be handed from one engine to another, so one is refused with an error.
|
|
16
|
+
|
|
17
|
+
## What `gdharness spread` tells a worker: which of how many it is.
|
|
18
|
+
const SHARE: String = "--gdh-share="
|
|
19
|
+
## What it tells the engine gathering: how many workers left outcomes.
|
|
20
|
+
const GATHER: String = "--gdh-share-gather="
|
|
21
|
+
## Where the workers leave their outcomes, an absolute path, since each engine has its own `user://`.
|
|
22
|
+
const INTO: String = "--gdh-share-dir="
|
|
23
|
+
## What a worker prints as it goes, which `gdharness spread` reads to say how far each has got.
|
|
24
|
+
const PROGRESS: String = "gdharness share: %s %d of %d runs"
|
|
25
|
+
|
|
26
|
+
static var _learnt: bool = false
|
|
27
|
+
static var _index: int = -1
|
|
28
|
+
static var _workers: int = 0
|
|
29
|
+
static var _gathering: int = 0
|
|
30
|
+
static var _directory: String = ""
|
|
31
|
+
static var _arms: Dictionary[String, bool] = {}
|
|
32
|
+
static var _read: bool = false
|
|
33
|
+
static var _unread: String = ""
|
|
34
|
+
static var _found: Dictionary[String, Dictionary] = {}
|
|
35
|
+
static var _found_runs: Dictionary[String, int] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Whether this engine is one of the workers of a spread run, which prints no table.
|
|
39
|
+
static func working() -> bool:
|
|
40
|
+
_learn()
|
|
41
|
+
return _index >= 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Whether this engine is the one gathering a spread run's outcomes.
|
|
45
|
+
static func gathering() -> bool:
|
|
46
|
+
_learn()
|
|
47
|
+
return _gathering > 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## The runs of [param runs] this engine plays: every one on its own, its share as a worker (every
|
|
51
|
+
## Nth from its own index, so each worker draws long runs and short ones alike), and none when it
|
|
52
|
+
## gathers.
|
|
53
|
+
static func dealt(runs: int) -> PackedInt32Array:
|
|
54
|
+
_learn()
|
|
55
|
+
var played: PackedInt32Array = []
|
|
56
|
+
if gathering():
|
|
57
|
+
return played
|
|
58
|
+
var step: int = _workers if working() else 1
|
|
59
|
+
for at: int in range(maxi(_index, 0), runs, step):
|
|
60
|
+
var _added: bool = played.append(at)
|
|
61
|
+
return played
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Every outcome of the arm [param arm]'s [param runs] runs, in run order: played here by
|
|
65
|
+
## [param play], which is called with a run's index, or gathered from the workers. A worker plays
|
|
66
|
+
## its share, leaves the outcomes for the engine gathering, and answers an empty array.
|
|
67
|
+
static func outcomes(arm: String, runs: int, play: Callable) -> Array:
|
|
68
|
+
_learn()
|
|
69
|
+
if _arms.has(arm):
|
|
70
|
+
push_error("GdhShare: the arm %s was played twice in one run; give each arm its own name" % arm)
|
|
71
|
+
return []
|
|
72
|
+
_arms[arm] = true
|
|
73
|
+
if gathering():
|
|
74
|
+
return _gathered(arm, runs)
|
|
75
|
+
|
|
76
|
+
var by_index: Dictionary[int, Variant] = {}
|
|
77
|
+
var share: PackedInt32Array = dealt(runs)
|
|
78
|
+
var every: int = maxi(ceili(share.size() / 10.0), 1)
|
|
79
|
+
for at: int in range(share.size()):
|
|
80
|
+
var run: int = share[at]
|
|
81
|
+
var outcome: Variant = play.call(run)
|
|
82
|
+
var wrong: String = _not_plain(outcome)
|
|
83
|
+
if not wrong.is_empty():
|
|
84
|
+
push_error(
|
|
85
|
+
(
|
|
86
|
+
"GdhShare: run %d of %s answered %s, which cannot be handed from one engine to another"
|
|
87
|
+
% [run, arm, wrong]
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
by_index[run] = outcome
|
|
91
|
+
if working() and ((at + 1) % every == 0 or at + 1 == share.size()):
|
|
92
|
+
print(PROGRESS % [arm, at + 1, share.size()])
|
|
93
|
+
|
|
94
|
+
if working():
|
|
95
|
+
_leave(arm, runs, by_index)
|
|
96
|
+
return []
|
|
97
|
+
var ordered: Array = []
|
|
98
|
+
for run: int in range(runs):
|
|
99
|
+
ordered.append(by_index[run])
|
|
100
|
+
return ordered
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## [param counted], each a dictionary of whole numbers, added up key by key; a key named in
|
|
104
|
+
## [param largest] keeps the largest instead. Adding and taking the largest come out the same in
|
|
105
|
+
## any order, which is what lets a bench fold its runs however it likes.
|
|
106
|
+
static func added(
|
|
107
|
+
counted: Array, largest: PackedStringArray = PackedStringArray()
|
|
108
|
+
) -> Dictionary[String, int]:
|
|
109
|
+
var total: Dictionary[String, int] = {}
|
|
110
|
+
for outcome: Variant in counted:
|
|
111
|
+
if not outcome is Dictionary:
|
|
112
|
+
push_error("GdhShare.added: an outcome is %s, not a dictionary" % type_string(typeof(outcome)))
|
|
113
|
+
continue
|
|
114
|
+
var counts: Dictionary = outcome
|
|
115
|
+
for key: Variant in counts:
|
|
116
|
+
var named: String = str(key)
|
|
117
|
+
var count: Variant = counts[key]
|
|
118
|
+
if not count is int:
|
|
119
|
+
push_error(
|
|
120
|
+
"GdhShare.added: %s is %s, not a whole number" % [named, type_string(typeof(count))]
|
|
121
|
+
)
|
|
122
|
+
continue
|
|
123
|
+
var now: int = count
|
|
124
|
+
if not total.has(named):
|
|
125
|
+
total[named] = now
|
|
126
|
+
elif largest.has(named):
|
|
127
|
+
total[named] = maxi(total[named], now)
|
|
128
|
+
else:
|
|
129
|
+
total[named] += now
|
|
130
|
+
return total
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Leaves one arm's outcomes, then the line saying so, which `gdharness spread` checks the share
|
|
134
|
+
## against; the outcomes first, so a line is never there without them.
|
|
135
|
+
static func _leave(arm: String, runs: int, by_index: Dictionary[int, Variant]) -> void:
|
|
136
|
+
var left: Dictionary = {"arm": arm, "runs": runs, "outcomes": by_index}
|
|
137
|
+
if not _appended("worker-%d.bin" % _index, func(file: FileAccess) -> bool: return file.store_var(left)):
|
|
138
|
+
return
|
|
139
|
+
var said: String = JSON.stringify({"arm": arm, "runs": runs, "played": by_index.size()})
|
|
140
|
+
var _noted: bool = _appended(
|
|
141
|
+
"worker-%d.arms" % _index, func(file: FileAccess) -> bool: return file.store_line(said)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## Appends to [param name] in the share directory with [param write], answering whether it did.
|
|
146
|
+
static func _appended(name: String, write: Callable) -> bool:
|
|
147
|
+
var path: String = _directory.path_join(name)
|
|
148
|
+
var file: FileAccess = FileAccess.open(
|
|
149
|
+
path, FileAccess.READ_WRITE if FileAccess.file_exists(path) else FileAccess.WRITE
|
|
150
|
+
)
|
|
151
|
+
if file == null:
|
|
152
|
+
push_error("GdhShare: cannot write %s: %s" % [path, error_string(FileAccess.get_open_error())])
|
|
153
|
+
return false
|
|
154
|
+
file.seek_end()
|
|
155
|
+
var wrote: bool = write.call(file)
|
|
156
|
+
file.close()
|
|
157
|
+
if not wrote:
|
|
158
|
+
push_error("GdhShare: cannot write %s" % path)
|
|
159
|
+
return wrote
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
static func _gathered(arm: String, runs: int) -> Array:
|
|
163
|
+
_read_workers()
|
|
164
|
+
if not _unread.is_empty():
|
|
165
|
+
push_error(_unread)
|
|
166
|
+
return []
|
|
167
|
+
if not _found.has(arm):
|
|
168
|
+
push_error(
|
|
169
|
+
(
|
|
170
|
+
(
|
|
171
|
+
"GdhShare: no worker played the arm %s, so this run went another way than theirs; "
|
|
172
|
+
+ "which arms a bench plays must depend on its command line alone"
|
|
173
|
+
)
|
|
174
|
+
% arm
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return []
|
|
178
|
+
if _found_runs[arm] != runs:
|
|
179
|
+
push_error(
|
|
180
|
+
(
|
|
181
|
+
"GdhShare: the workers played %d runs of %s, and this run asks for %d"
|
|
182
|
+
% [_found_runs[arm], arm, runs]
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
return []
|
|
186
|
+
var held: Dictionary = _found[arm]
|
|
187
|
+
var ordered: Array = []
|
|
188
|
+
for run: int in range(runs):
|
|
189
|
+
if not held.has(run):
|
|
190
|
+
push_error("GdhShare: no worker played run %d of %s" % [run, arm])
|
|
191
|
+
return []
|
|
192
|
+
ordered.append(held[run])
|
|
193
|
+
var said: String = JSON.stringify({"arm": arm, "runs": runs, "played": runs})
|
|
194
|
+
var _noted: bool = _appended(
|
|
195
|
+
"gathered.arms", func(file: FileAccess) -> bool: return file.store_line(said)
|
|
196
|
+
)
|
|
197
|
+
return ordered
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
static func _read_workers() -> void:
|
|
201
|
+
if _read:
|
|
202
|
+
return
|
|
203
|
+
_read = true
|
|
204
|
+
for worker: int in range(_gathering):
|
|
205
|
+
var path: String = _directory.path_join("worker-%d.bin" % worker)
|
|
206
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
207
|
+
if file == null:
|
|
208
|
+
_unread = "GdhShare: cannot read %s: %s" % [path, error_string(FileAccess.get_open_error())]
|
|
209
|
+
return
|
|
210
|
+
while file.get_position() < file.get_length():
|
|
211
|
+
_unread = _took(file.get_var(), path)
|
|
212
|
+
if not _unread.is_empty():
|
|
213
|
+
file.close()
|
|
214
|
+
return
|
|
215
|
+
file.close()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
## Adds one arm a worker left to what has been found, answering why it cannot, or empty.
|
|
219
|
+
static func _took(left: Variant, path: String) -> String:
|
|
220
|
+
if not left is Dictionary:
|
|
221
|
+
return "GdhShare: %s holds something other than a worker's outcomes" % path
|
|
222
|
+
var record: Dictionary = left
|
|
223
|
+
var arm_said: Variant = record.get("arm")
|
|
224
|
+
var runs_said: Variant = record.get("runs")
|
|
225
|
+
var outcomes_said: Variant = record.get("outcomes")
|
|
226
|
+
if not (arm_said is String and runs_said is int and outcomes_said is Dictionary):
|
|
227
|
+
return "GdhShare: %s holds something other than a worker's outcomes" % path
|
|
228
|
+
var arm: String = arm_said
|
|
229
|
+
var runs: int = runs_said
|
|
230
|
+
var played: Dictionary = outcomes_said
|
|
231
|
+
if _found_runs.has(arm) and _found_runs[arm] != runs:
|
|
232
|
+
return (
|
|
233
|
+
"GdhShare: the workers disagree on how many runs %s has: %d and %d"
|
|
234
|
+
% [arm, _found_runs[arm], runs]
|
|
235
|
+
)
|
|
236
|
+
_found_runs[arm] = runs
|
|
237
|
+
var held: Dictionary = _found.get_or_add(arm, {})
|
|
238
|
+
for run: Variant in played:
|
|
239
|
+
if held.has(run):
|
|
240
|
+
return "GdhShare: run %s of %s was played by two workers" % [str(run), arm]
|
|
241
|
+
held[run] = played[run]
|
|
242
|
+
return ""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
static func _learn() -> void:
|
|
246
|
+
if _learnt:
|
|
247
|
+
return
|
|
248
|
+
_learnt = true
|
|
249
|
+
for given: String in OS.get_cmdline_user_args():
|
|
250
|
+
if given.begins_with(SHARE):
|
|
251
|
+
var halves: PackedStringArray = given.trim_prefix(SHARE).split("/")
|
|
252
|
+
if halves.size() == 2 and halves[0].is_valid_int() and halves[1].is_valid_int():
|
|
253
|
+
_index = halves[0].to_int()
|
|
254
|
+
_workers = halves[1].to_int()
|
|
255
|
+
if _index < 0 or _index >= _workers:
|
|
256
|
+
push_error("GdhShare: %s names no worker; it takes <index>/<workers>" % given)
|
|
257
|
+
_index = -1
|
|
258
|
+
elif given.begins_with(GATHER):
|
|
259
|
+
_gathering = given.trim_prefix(GATHER).to_int()
|
|
260
|
+
elif given.begins_with(INTO):
|
|
261
|
+
_directory = given.trim_prefix(INTO)
|
|
262
|
+
if (_index >= 0 or _gathering > 0) and _directory.is_empty():
|
|
263
|
+
push_error("GdhShare: a spread engine was given no %s<directory> to share outcomes in" % INTO)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
## What in [param value] cannot be written by one engine and read by another, or empty.
|
|
267
|
+
static func _not_plain(value: Variant) -> String:
|
|
268
|
+
match typeof(value):
|
|
269
|
+
TYPE_OBJECT:
|
|
270
|
+
return "an object"
|
|
271
|
+
TYPE_CALLABLE:
|
|
272
|
+
return "a callable"
|
|
273
|
+
TYPE_SIGNAL:
|
|
274
|
+
return "a signal"
|
|
275
|
+
TYPE_RID:
|
|
276
|
+
return "a resource id"
|
|
277
|
+
TYPE_ARRAY:
|
|
278
|
+
var items: Array = value
|
|
279
|
+
for item: Variant in items:
|
|
280
|
+
var wrong: String = _not_plain(item)
|
|
281
|
+
if not wrong.is_empty():
|
|
282
|
+
return wrong
|
|
283
|
+
TYPE_DICTIONARY:
|
|
284
|
+
var pairs: Dictionary = value
|
|
285
|
+
for key: Variant in pairs:
|
|
286
|
+
var wrong: String = _not_plain(key) + _not_plain(pairs[key])
|
|
287
|
+
if not wrong.is_empty():
|
|
288
|
+
return wrong
|
|
289
|
+
return ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bxqhnwjb1qap8
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
|
|
3
|
+
## gdUnit4 suites run inside a played scene, which the engine's script profiler sees and a script
|
|
4
|
+
## run with `-s` never does. `editor_run start` given `suites` plays this scene with
|
|
5
|
+
## [constant RUNNER] naming the runner `gdharness test` uses, where the server ships it, followed by
|
|
6
|
+
## that runner's own arguments. The runner is loaded from there rather than kept in this addon,
|
|
7
|
+
## since it extends gdUnit4's classes and a project without gdUnit4 must still compile the addon.
|
|
8
|
+
|
|
9
|
+
const RUNNER: String = "--gdh-runner"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
func _ready() -> void:
|
|
13
|
+
var asked: PackedStringArray = OS.get_cmdline_user_args()
|
|
14
|
+
var at: int = asked.find(RUNNER)
|
|
15
|
+
var path: String = asked[at + 1] if at >= 0 and at + 1 < asked.size() else ""
|
|
16
|
+
var listed: GDScript = null
|
|
17
|
+
if not path.is_empty() and FileAccess.file_exists(path):
|
|
18
|
+
listed = load(path) as GDScript
|
|
19
|
+
if listed == null:
|
|
20
|
+
push_error(
|
|
21
|
+
(
|
|
22
|
+
(
|
|
23
|
+
"gdharness suites: no runner at '%s'. This scene is played by editor_run start with suites,"
|
|
24
|
+
+ " which names the runner after %s; it does nothing played any other way."
|
|
25
|
+
)
|
|
26
|
+
% [path, RUNNER]
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
get_tree().quit(1)
|
|
30
|
+
return
|
|
31
|
+
var runner: Node = listed.call("started", asked.slice(at + 2))
|
|
32
|
+
# The runner holds its own script past the engine's shutdown by this path.
|
|
33
|
+
runner.set("own_path", path)
|
|
34
|
+
add_child(runner)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bq3n7xkd2wfs0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class_name GdhTweens
|
|
2
|
+
extends RefCounted
|
|
3
|
+
## Moves the tree's animations on by a time the case chooses, rather than waiting for them.
|
|
4
|
+
##
|
|
5
|
+
## A case that waits a real timer of an animation's length races the animation: the tree runs its
|
|
6
|
+
## timers before its tweens, so on a busy machine the timer fires ahead of the last step and the
|
|
7
|
+
## case reads a fade short of its end. Stepping the tweens reads the slide or the fade exactly
|
|
8
|
+
## where the case asks.
|
|
9
|
+
|
|
10
|
+
## A frame at sixty a second.
|
|
11
|
+
const FRAME: float = 1.0 / 60.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Steps every tween the tree is processing by [param seconds], as frames that long would.
|
|
15
|
+
static func step(tree: SceneTree, seconds: float) -> void:
|
|
16
|
+
for running: Tween in tree.get_processed_tweens():
|
|
17
|
+
var _stepped: bool = running.custom_step(seconds)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [method step] [param seconds] and one frame more, for a case about what an animation leaves once
|
|
21
|
+
## it is over: a step that ends exactly where the last tweener does leaves a callback after it
|
|
22
|
+
## waiting for time it has not been given.
|
|
23
|
+
static func past(tree: SceneTree, seconds: float) -> void:
|
|
24
|
+
step(tree, seconds + FRAME)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://dlsfeajehdbca
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class_name GdhWait
|
|
2
|
+
extends RefCounted
|
|
3
|
+
## Waits out what the game is doing by asking every frame whether it is done, never by a timer.
|
|
4
|
+
##
|
|
5
|
+
## A timer of an animation's length and a margin is a race the animation loses on a busy machine:
|
|
6
|
+
## the engine counts a tween's time in the frames' deltas, and a frame that waits on other engines
|
|
7
|
+
## arrives late, so the timer ends with the tween still running. Asking once a frame holds however
|
|
8
|
+
## slowly the frames come. The bound is on the clock only so that something that never ends fails
|
|
9
|
+
## the case by name instead of holding the run until its silence ends it.
|
|
10
|
+
|
|
11
|
+
## How long [method until] and [method still] wait by default, in milliseconds.
|
|
12
|
+
const MOST_MS: int = 30000
|
|
13
|
+
|
|
14
|
+
## Frames in a row with no tween running before [method still] counts the tree as still. A layout
|
|
15
|
+
## often starts its slide a frame after the change that asks for it, so the frame straight after
|
|
16
|
+
## had no tween yet and passed for still.
|
|
17
|
+
const QUIET_FRAMES: int = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Whether [param done] answered true within [param most_ms], asked now and then once a frame.
|
|
21
|
+
static func until(tree: SceneTree, done: Callable, most_ms: int = MOST_MS) -> bool:
|
|
22
|
+
var began: int = Time.get_ticks_msec()
|
|
23
|
+
while not done.call():
|
|
24
|
+
if Time.get_ticks_msec() - began > most_ms:
|
|
25
|
+
return false
|
|
26
|
+
await tree.process_frame
|
|
27
|
+
return true
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Whether the tree ran no tween for [param quiet_frames] frames in a row within [param most_ms].
|
|
31
|
+
static func still(tree: SceneTree, quiet_frames: int = QUIET_FRAMES, most_ms: int = MOST_MS) -> bool:
|
|
32
|
+
var began: int = Time.get_ticks_msec()
|
|
33
|
+
var quiet: int = 0
|
|
34
|
+
while quiet < quiet_frames:
|
|
35
|
+
if Time.get_ticks_msec() - began > most_ms:
|
|
36
|
+
return false
|
|
37
|
+
await tree.process_frame
|
|
38
|
+
quiet = quiet + 1 if not running(tree) else 0
|
|
39
|
+
return true
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## [param count] frames of the tree, waited one at a time.
|
|
43
|
+
static func frames(tree: SceneTree, count: int) -> void:
|
|
44
|
+
for _frame: int in range(count):
|
|
45
|
+
await tree.process_frame
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Whether any tween under [param tree] is running.
|
|
49
|
+
static func running(tree: SceneTree) -> bool:
|
|
50
|
+
for tween: Tween in tree.get_processed_tweens():
|
|
51
|
+
if tween.is_running():
|
|
52
|
+
return true
|
|
53
|
+
return false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://wq6ytbgoow47
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
## Brings gdharness's runtime up in a debug build that has its addon, and does nothing anywhere else.
|
|
3
|
+
##
|
|
4
|
+
## `project.godot` names this file rather than the addon's script. `addons/` is often not committed
|
|
5
|
+
## and exports leave it out, so an entry naming the addon boots a fresh clone or an exported build
|
|
6
|
+
## with a missing script. `gdharness setup` writes this file, and `gdharness doctor` says when it is
|
|
7
|
+
## not the one that gdharness ships. The runtime refuses a `-s` script run by itself unless
|
|
8
|
+
## `gdharness/runtime/serve_script_runs` is on, so that is left to it.
|
|
9
|
+
|
|
10
|
+
## Loaded by path as it runs rather than preloaded: a preload resolves when this script is parsed,
|
|
11
|
+
## and in a build without the addon that is the missing script this file is here to avoid.
|
|
12
|
+
const RUNTIME: String = "res://addons/gdharness_runtime/runtime_autoload.gd"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
func _ready() -> void:
|
|
16
|
+
if not OS.is_debug_build() or not ResourceLoader.exists(RUNTIME):
|
|
17
|
+
return
|
|
18
|
+
var script: GDScript = load(RUNTIME) as GDScript
|
|
19
|
+
if script == null:
|
|
20
|
+
return
|
|
21
|
+
var made: Object = script.new()
|
|
22
|
+
var runtime: Node = made as Node
|
|
23
|
+
if runtime == null:
|
|
24
|
+
return
|
|
25
|
+
# Under this node rather than beside it: the root refuses a child while it is still adding the
|
|
26
|
+
# autoloads, and a deferred add would bring the runtime up only after the main scene's _ready.
|
|
27
|
+
# Added now, it announces before the main scene runs, as it does when an entry names it.
|
|
28
|
+
add_child(runtime)
|
|
@@ -26,7 +26,7 @@ func refresh_class_cache(_params: Dictionary) -> Dictionary:
|
|
|
26
26
|
|
|
27
27
|
var entries: Array = []
|
|
28
28
|
var skipped: Array[Dictionary] = []
|
|
29
|
-
for path: String in _files.find_files("res://", ".gd"):
|
|
29
|
+
for path: String in _files.find_files("res://", ".gd", true):
|
|
30
30
|
var entry: Dictionary = _entry_for(path, skipped)
|
|
31
31
|
if not entry.is_empty():
|
|
32
32
|
entries.append(entry)
|
|
@@ -77,10 +77,13 @@ func is_installed_by_gdharness(path: String) -> bool:
|
|
|
77
77
|
return FileAccess.file_exists(path + ".gdharness-version")
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
# Files under `path` whose name ends with `extension`, which is passed with its dot.
|
|
81
|
-
|
|
80
|
+
# Files under `path` whose name ends with `extension`, which is passed with its dot. An addon
|
|
81
|
+
# gdharness installed is walked into when [param with_ours] is set, for a caller rebuilding what
|
|
82
|
+
# the engine itself would: the engine lists the global classes of every addon, and a list rebuilt
|
|
83
|
+
# without ours dropped the one class the tests addon declares.
|
|
84
|
+
func find_files(path: String, extension: String, with_ours: bool = false) -> Array[String]:
|
|
82
85
|
var files: Array[String] = []
|
|
83
|
-
if is_stepped_over(path) or is_installed_by_gdharness(path):
|
|
86
|
+
if is_stepped_over(path) or (not with_ours and is_installed_by_gdharness(path)):
|
|
84
87
|
return files
|
|
85
88
|
|
|
86
89
|
var dir: DirAccess = DirAccess.open(path)
|
|
@@ -92,7 +95,7 @@ func find_files(path: String, extension: String) -> Array[String]:
|
|
|
92
95
|
|
|
93
96
|
while file_name != "":
|
|
94
97
|
if dir.current_is_dir() and not file_name.begins_with("."):
|
|
95
|
-
files.append_array(find_files(path + file_name + "/", extension))
|
|
98
|
+
files.append_array(find_files(path + file_name + "/", extension, with_ours))
|
|
96
99
|
elif file_name.ends_with(extension):
|
|
97
100
|
files.append(path + file_name)
|
|
98
101
|
|
|
@@ -191,7 +191,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
191
191
|
# Through ProjectSettings so the file is saved the way the editor saves it, header and
|
|
192
192
|
# every other line kept; a ConfigFile of project.godot drops the comments on the way out.
|
|
193
193
|
ProjectSettings.set_setting(setting, action)
|
|
194
|
-
var err: Error = GdhProjectFile.save()
|
|
194
|
+
var err: Error = GdhProjectFile.save([setting])
|
|
195
195
|
if err != OK:
|
|
196
196
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
197
197
|
|
|
@@ -297,7 +297,7 @@ func remove_input_action(params: Dictionary) -> Dictionary:
|
|
|
297
297
|
)
|
|
298
298
|
)
|
|
299
299
|
ProjectSettings.set_setting(setting, null)
|
|
300
|
-
var err: Error = GdhProjectFile.save()
|
|
300
|
+
var err: Error = GdhProjectFile.save([setting])
|
|
301
301
|
if err != OK:
|
|
302
302
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
303
303
|
return {"action_name": action_name, "removed": true}
|
|
@@ -172,11 +172,11 @@ static func _names_of(paths: Array[String]) -> Array[String]:
|
|
|
172
172
|
func _save_enabled_paths(paths: Array[String]) -> Error:
|
|
173
173
|
if paths.is_empty():
|
|
174
174
|
ProjectSettings.set_setting("editor_plugins/enabled", null)
|
|
175
|
-
return GdhProjectFile.save()
|
|
175
|
+
return GdhProjectFile.save(["editor_plugins/enabled"])
|
|
176
176
|
|
|
177
177
|
# Kept in an Array[String] and converted here, because appending to a packed array answers with
|
|
178
178
|
# whether it worked and a project holding return_value_discarded at error level will not compile
|
|
179
179
|
# a script that drops that answer. The conversion is what matters: an Array[String] handed to
|
|
180
180
|
# set_setting is written as Array[String]([...]), which the editor does not read as a plugin list.
|
|
181
181
|
ProjectSettings.set_setting("editor_plugins/enabled", PackedStringArray(paths))
|
|
182
|
-
return GdhProjectFile.save()
|
|
182
|
+
return GdhProjectFile.save(["editor_plugins/enabled"])
|
|
@@ -47,9 +47,11 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
47
47
|
old_value = ProjectSettings.get_setting(setting_path)
|
|
48
48
|
# A setting the engine registers is there with its default whether or not project.godot sets it,
|
|
49
49
|
# so has_setting alone called the description of a project that had never set one an old value.
|
|
50
|
-
#
|
|
50
|
+
# Whether it is new is the file's to say: one at its default has a line only where the project
|
|
51
|
+
# wrote it out on purpose.
|
|
51
52
|
var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
|
|
52
53
|
var was_default: bool = had_value and default_value != null and old_value == default_value
|
|
54
|
+
var in_file: bool = GdhProjectFile.sets(setting_path)
|
|
53
55
|
|
|
54
56
|
var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
|
|
55
57
|
# A value of fields or a colour's html code is fitted as a property write fits it, so a value one
|
|
@@ -68,12 +70,18 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
68
70
|
var number: float = final_value
|
|
69
71
|
if number == floorf(number) and absf(number) < 9.0e15:
|
|
70
72
|
final_value = int(number)
|
|
73
|
+
# A tag that could not be built is still the dictionary it arrived as, and a setting nothing
|
|
74
|
+
# declares took it as one: the file was given the tag key by key, beside an answer of success.
|
|
75
|
+
if final_value is Dictionary:
|
|
76
|
+
var held: Dictionary = final_value
|
|
77
|
+
if held.has("_type"):
|
|
78
|
+
return _log.failure(_unbuilt(setting_path, held))
|
|
71
79
|
|
|
72
80
|
var answer: Dictionary = {
|
|
73
81
|
"setting_path": setting_path,
|
|
74
82
|
"old_value": _values.serialize_value(old_value) if had_value else null,
|
|
75
83
|
"new_value": _values.serialize_value(final_value),
|
|
76
|
-
"was_new": not
|
|
84
|
+
"was_new": not in_file,
|
|
77
85
|
"was_default": was_default,
|
|
78
86
|
"saved": true,
|
|
79
87
|
}
|
|
@@ -82,7 +90,7 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
82
90
|
if had_value and typeof(old_value) == typeof(final_value) and old_value == final_value:
|
|
83
91
|
answer["saved"] = false
|
|
84
92
|
answer["unchanged"] = true
|
|
85
|
-
if was_default:
|
|
93
|
+
if was_default and not in_file:
|
|
86
94
|
answer["note"] = (
|
|
87
95
|
(
|
|
88
96
|
"%s is already this setting's default, which project.godot holds no line for:"
|
|
@@ -122,7 +130,7 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
122
130
|
said += ("; project_import reimport makes the translation files from " + ", ".join(made))
|
|
123
131
|
answer["note"] = (str(answer["note"]) + " " if answer.has("note") else "") + said + "."
|
|
124
132
|
ProjectSettings.set_setting(setting_path, final_value)
|
|
125
|
-
var err: Error = GdhProjectFile.save()
|
|
133
|
+
var err: Error = GdhProjectFile.save([setting_path])
|
|
126
134
|
if err != OK:
|
|
127
135
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
128
136
|
return answer
|
|
@@ -180,8 +188,9 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
|
180
188
|
"setting_path": setting_path,
|
|
181
189
|
"default_value": _values.serialize_value(default_value) if default_value != null else null,
|
|
182
190
|
}
|
|
183
|
-
#
|
|
184
|
-
|
|
191
|
+
# Read off the file rather than the value: a setting at its default usually has no line, as the
|
|
192
|
+
# engine leaves it out, but a project can hold one written out on purpose.
|
|
193
|
+
if not held or not GdhProjectFile.sets(setting_path):
|
|
185
194
|
answer["removed"] = false
|
|
186
195
|
answer["saved"] = false
|
|
187
196
|
answer["note"] = "project.godot does not set %s, so there was nothing to take out." % setting_path
|
|
@@ -190,7 +199,7 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
|
|
|
190
199
|
# The default rather than null for a setting the engine declares: null takes the declaration away
|
|
191
200
|
# as well, and a value at its default is left out of the file all the same.
|
|
192
201
|
ProjectSettings.set_setting(setting_path, default_value)
|
|
193
|
-
var err: Error = GdhProjectFile.save()
|
|
202
|
+
var err: Error = GdhProjectFile.save([setting_path])
|
|
194
203
|
if err != OK:
|
|
195
204
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
196
205
|
answer["removed"] = true
|
|
@@ -266,6 +275,24 @@ static func _cannot_hold(setting_path: String, wanted: int, value: Variant) -> S
|
|
|
266
275
|
return "%s, and %s (%s) cannot become one.%s" % [declared, JSON.stringify(value), given, remedy]
|
|
267
276
|
|
|
268
277
|
|
|
278
|
+
## Why [param tagged], a value whose tag could not be built, was not written to [param setting_path],
|
|
279
|
+
## and how a value of that tag is written.
|
|
280
|
+
static func _unbuilt(setting_path: String, tagged: Dictionary) -> String:
|
|
281
|
+
var tag: String = str(tagged["_type"])
|
|
282
|
+
var refused: String = (
|
|
283
|
+
"The value for %s is tagged %s and could not be read as one, so nothing was written."
|
|
284
|
+
% [setting_path, JSON.stringify(tag)]
|
|
285
|
+
)
|
|
286
|
+
var shapes: Array = GdhShapes.REQUIRED.get(tag, [])
|
|
287
|
+
if shapes.is_empty():
|
|
288
|
+
return refused + " A setting cannot hold a %s written as a tag." % tag
|
|
289
|
+
var keys: Array = shapes[0]
|
|
290
|
+
var parts: Array[String] = ['"_type": "%s"' % tag]
|
|
291
|
+
for key: Variant in keys:
|
|
292
|
+
parts.append('"%s": %s' % [key, "[...]" if tag.begins_with("Packed") and key == "value" else "..."])
|
|
293
|
+
return refused + " One is written as {%s}." % ", ".join(parts)
|
|
294
|
+
|
|
295
|
+
|
|
269
296
|
# Add an autoload singleton
|
|
270
297
|
func add_autoload(params: Dictionary) -> Dictionary:
|
|
271
298
|
var name: String = str(params.get("name", ""))
|
|
@@ -313,7 +340,7 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
313
340
|
"saved": false
|
|
314
341
|
}
|
|
315
342
|
ProjectSettings.set_setting(setting, entry)
|
|
316
|
-
var err: Error = GdhProjectFile.save()
|
|
343
|
+
var err: Error = GdhProjectFile.save([setting])
|
|
317
344
|
if err != OK:
|
|
318
345
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
319
346
|
|
|
@@ -339,7 +366,7 @@ func remove_autoload(params: Dictionary) -> Dictionary:
|
|
|
339
366
|
|
|
340
367
|
var old_value: String = str(ProjectSettings.get_setting(setting))
|
|
341
368
|
ProjectSettings.set_setting(setting, null)
|
|
342
|
-
var err: Error = GdhProjectFile.save()
|
|
369
|
+
var err: Error = GdhProjectFile.save([setting])
|
|
343
370
|
if err != OK:
|
|
344
371
|
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
345
372
|
|
|
@@ -401,7 +428,7 @@ func set_main_scene(params: Dictionary) -> Dictionary:
|
|
|
401
428
|
var old_main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
|
|
402
429
|
|
|
403
430
|
ProjectSettings.set_setting("application/run/main_scene", scene_path)
|
|
404
|
-
var err: Error = GdhProjectFile.save()
|
|
431
|
+
var err: Error = GdhProjectFile.save(["application/run/main_scene"])
|
|
405
432
|
|
|
406
433
|
if err != OK:
|
|
407
434
|
return _log.failure("Failed to save project settings: " + str(err))
|