gdharness 1.3.12 → 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 +32841 -31861
- 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 +130 -19
- package/build/keeper.js +8401 -931
- package/package.json +1 -1
|
@@ -5,21 +5,25 @@ extends RefCounted
|
|
|
5
5
|
## The engine writes each section's keys in the order it registered them, and an engine running a
|
|
6
6
|
## script registers them in another order than the editor does, so a one-setting change moved lines
|
|
7
7
|
## nobody touched across several sections and a commit of it read as a rewrite. The save is still the
|
|
8
|
-
## engine's, for what only it gets right: the header
|
|
9
|
-
##
|
|
8
|
+
## engine's, for what only it gets right: the header and each value's text. The order is taken from
|
|
9
|
+
## the file as it was, and so is every line of a key the call did not change: the engine leaves out
|
|
10
|
+
## each key at its default, so a project holding `unsafe_call_argument=0` on purpose lost the line to
|
|
11
|
+
## a change of another key.
|
|
10
12
|
|
|
11
13
|
const PATH: String = "res://project.godot"
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
## Saves the project's settings, keeping the sections and keys already in the file where they were.
|
|
15
|
-
## A key or a section new to the file goes where the engine put it relative to its neighbours.
|
|
16
|
-
|
|
17
|
+
## A key or a section new to the file goes where the engine put it relative to its neighbours. Only
|
|
18
|
+
## the settings in [param changed] may leave the file, as the engine leaves out one set back to its
|
|
19
|
+
## default or erased.
|
|
20
|
+
static func save(changed: PackedStringArray) -> Error:
|
|
17
21
|
var before: String = FileAccess.get_file_as_string(PATH)
|
|
18
22
|
var err: Error = ProjectSettings.save()
|
|
19
23
|
if err != OK or before.is_empty():
|
|
20
24
|
return err
|
|
21
25
|
var written: String = FileAccess.get_file_as_string(PATH)
|
|
22
|
-
var kept: String = in_order_of(before, written)
|
|
26
|
+
var kept: String = in_order_of(before, written, changed)
|
|
23
27
|
if kept == written:
|
|
24
28
|
return OK
|
|
25
29
|
var file: FileAccess = FileAccess.open(PATH, FileAccess.WRITE)
|
|
@@ -31,14 +35,33 @@ static func save() -> Error:
|
|
|
31
35
|
return OK
|
|
32
36
|
|
|
33
37
|
|
|
34
|
-
## [param written] with its sections and their keys put in the order [param before] has them
|
|
35
|
-
## line
|
|
36
|
-
|
|
38
|
+
## [param written] with its sections and their keys put in the order [param before] has them, and
|
|
39
|
+
## each line [param before] has for a key [param written] lacks put back, unless that key's setting
|
|
40
|
+
## is in [param changed]. Every other line of the result is a line of [param written], so nothing the
|
|
41
|
+
## engine wrote is lost or changed.
|
|
42
|
+
static func in_order_of(before: String, written: String, changed: PackedStringArray) -> String:
|
|
37
43
|
var old: Dictionary = _parsed(before)
|
|
38
44
|
var fresh: Dictionary = _parsed(written)
|
|
39
45
|
var old_sections: Array[Dictionary] = old["sections"]
|
|
40
46
|
var fresh_sections: Array[Dictionary] = fresh["sections"]
|
|
41
47
|
|
|
48
|
+
var by_name: Dictionary[String, Dictionary] = {}
|
|
49
|
+
var fresh_names: Array[String] = []
|
|
50
|
+
var entry_of: Dictionary[String, Dictionary] = {}
|
|
51
|
+
var fresh_keys: Dictionary[String, Array] = {}
|
|
52
|
+
for section: Dictionary in fresh_sections:
|
|
53
|
+
var name: String = section["name"]
|
|
54
|
+
by_name[name] = section
|
|
55
|
+
fresh_names.append(name)
|
|
56
|
+
var texts: Dictionary[String, String] = {}
|
|
57
|
+
var keys: Array[String] = []
|
|
58
|
+
for entry: Dictionary in section["entries"]:
|
|
59
|
+
var key: String = entry["key"]
|
|
60
|
+
texts[key] = str(entry["text"])
|
|
61
|
+
keys.append(key)
|
|
62
|
+
entry_of[name] = texts
|
|
63
|
+
fresh_keys[name] = keys
|
|
64
|
+
|
|
42
65
|
var old_names: Array[String] = []
|
|
43
66
|
var old_keys: Dictionary[String, Array] = {}
|
|
44
67
|
for section: Dictionary in old_sections:
|
|
@@ -48,39 +71,74 @@ static func in_order_of(before: String, written: String) -> String:
|
|
|
48
71
|
for entry: Dictionary in section["entries"]:
|
|
49
72
|
keys.append(str(entry["key"]))
|
|
50
73
|
old_keys[name] = keys
|
|
74
|
+
var kept: Array[Dictionary] = []
|
|
75
|
+
for entry: Dictionary in section["entries"]:
|
|
76
|
+
var key: String = entry["key"]
|
|
77
|
+
if (
|
|
78
|
+
not (by_name.has(name) and entry_of[name].has(key))
|
|
79
|
+
and not changed.has(_setting_of(name, key))
|
|
80
|
+
):
|
|
81
|
+
kept.append(entry)
|
|
82
|
+
if kept.is_empty():
|
|
83
|
+
continue
|
|
84
|
+
if not by_name.has(name):
|
|
85
|
+
by_name[name] = section
|
|
86
|
+
fresh_names.append(name)
|
|
87
|
+
entry_of[name] = {}
|
|
88
|
+
fresh_keys[name] = []
|
|
89
|
+
for entry: Dictionary in kept:
|
|
90
|
+
entry_of[name][str(entry["key"])] = str(entry["text"])
|
|
91
|
+
fresh_keys[name].append(str(entry["key"]))
|
|
51
92
|
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
# The blank lines closing a section belong to its place in the file rather than to the section,
|
|
94
|
+
# so the last one keeps the ending the engine gave the file wherever it moved from. A section put
|
|
95
|
+
# back sits between two others, which the engine parts with a blank line.
|
|
96
|
+
var names: Array[String] = _merged(old_names, fresh_names)
|
|
97
|
+
var tails: Array[String] = []
|
|
54
98
|
for section: Dictionary in fresh_sections:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
tails.append(str(section["tail"]))
|
|
100
|
+
while tails.size() < names.size():
|
|
101
|
+
# Not checked: a position inside the array cannot be refused.
|
|
102
|
+
var _inserted: int = tails.insert(maxi(tails.size() - 1, 0), "\n")
|
|
58
103
|
|
|
59
104
|
var text: String = fresh["preamble"]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
var section: Dictionary = by_name[name]
|
|
63
|
-
var entries: Array[Dictionary] = section["entries"]
|
|
64
|
-
var entry_of: Dictionary[String, String] = {}
|
|
65
|
-
var fresh_keys: Array[String] = []
|
|
66
|
-
for entry: Dictionary in entries:
|
|
67
|
-
var key: String = entry["key"]
|
|
68
|
-
entry_of[key] = str(entry["text"])
|
|
69
|
-
fresh_keys.append(key)
|
|
105
|
+
for at: int in names.size():
|
|
106
|
+
var name: String = names[at]
|
|
70
107
|
var known: Array = old_keys.get(name, [])
|
|
71
108
|
var held: Array[String] = []
|
|
72
109
|
held.assign(known)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
text +=
|
|
80
|
-
at += 1
|
|
110
|
+
var listed: Array = fresh_keys[name]
|
|
111
|
+
var wanted: Array[String] = []
|
|
112
|
+
wanted.assign(listed)
|
|
113
|
+
text += str(by_name[name]["head"])
|
|
114
|
+
for key: String in _merged(held, wanted):
|
|
115
|
+
text += str(entry_of[name][key])
|
|
116
|
+
text += tails[at]
|
|
81
117
|
return text
|
|
82
118
|
|
|
83
119
|
|
|
120
|
+
## The setting [param key] in section [param section] stands for, as the engine names it: the
|
|
121
|
+
## section and the key joined, a quoted key read without its quotes.
|
|
122
|
+
static func _setting_of(section: String, key: String) -> String:
|
|
123
|
+
var bare: String = key
|
|
124
|
+
if bare.length() >= 2 and bare.begins_with('"') and bare.ends_with('"'):
|
|
125
|
+
bare = bare.substr(1, bare.length() - 2).c_unescape()
|
|
126
|
+
return bare if section.is_empty() else section + "/" + bare
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## Whether project.godot has a line for [param setting_path]. A setting at its default is in the
|
|
130
|
+
## file only when the file says so, which the value alone cannot tell.
|
|
131
|
+
static func sets(setting_path: String) -> bool:
|
|
132
|
+
var parsed: Dictionary = _parsed(FileAccess.get_file_as_string(PATH))
|
|
133
|
+
var sections: Array[Dictionary] = parsed["sections"]
|
|
134
|
+
for section: Dictionary in sections:
|
|
135
|
+
var entries: Array[Dictionary] = section["entries"]
|
|
136
|
+
for entry: Dictionary in entries:
|
|
137
|
+
if _setting_of(str(section["name"]), str(entry["key"])) == setting_path:
|
|
138
|
+
return true
|
|
139
|
+
return false
|
|
140
|
+
|
|
141
|
+
|
|
84
142
|
## The names of [param wanted] in the order [param held] lists them, those [param held] lacks each
|
|
85
143
|
## placed after the name [param wanted] puts before it. Names [param wanted] lacks are left out.
|
|
86
144
|
static func _merged(held: Array[String], wanted: Array[String]) -> Array[String]:
|
|
@@ -222,6 +222,8 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
222
222
|
# to compile a script that gives a Variant to a typed parameter: the operations are compiled
|
|
223
223
|
# under the target project's warning levels, so one such project lost every headless call.
|
|
224
224
|
var tag: String = str(fields["_type"])
|
|
225
|
+
if GdhShapes.LIST_TAGS.has(tag):
|
|
226
|
+
return GdhShapes.packed_tagged(fields, fitted)
|
|
225
227
|
var builder: String = BUILDERS.get(tag, "")
|
|
226
228
|
if builder.is_empty() or not GdhShapes.buildable(fields, tag):
|
|
227
229
|
return fields
|
|
@@ -20,6 +20,18 @@ const PACKED_ELEMENTS: Dictionary = {
|
|
|
20
20
|
TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
|
|
21
21
|
TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
|
|
22
22
|
}
|
|
23
|
+
## The tags of the packed arrays that cross as a plain list, and the type each builds.
|
|
24
|
+
const LIST_TAGS: Dictionary = {
|
|
25
|
+
"PackedInt32Array": TYPE_PACKED_INT32_ARRAY,
|
|
26
|
+
"PackedInt64Array": TYPE_PACKED_INT64_ARRAY,
|
|
27
|
+
"PackedFloat32Array": TYPE_PACKED_FLOAT32_ARRAY,
|
|
28
|
+
"PackedFloat64Array": TYPE_PACKED_FLOAT64_ARRAY,
|
|
29
|
+
"PackedStringArray": TYPE_PACKED_STRING_ARRAY,
|
|
30
|
+
"PackedVector2Array": TYPE_PACKED_VECTOR2_ARRAY,
|
|
31
|
+
"PackedVector3Array": TYPE_PACKED_VECTOR3_ARRAY,
|
|
32
|
+
"PackedVector4Array": TYPE_PACKED_VECTOR4_ARRAY,
|
|
33
|
+
"PackedColorArray": TYPE_PACKED_COLOR_ARRAY,
|
|
34
|
+
}
|
|
23
35
|
|
|
24
36
|
# What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
|
|
25
37
|
# tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
|
|
@@ -44,6 +56,15 @@ const REQUIRED: Dictionary = {
|
|
|
44
56
|
"Color": [["r", "g", "b"]],
|
|
45
57
|
"NodePath": [["path"]],
|
|
46
58
|
"PackedByteArray": [["base64"]],
|
|
59
|
+
"PackedInt32Array": [["value"]],
|
|
60
|
+
"PackedInt64Array": [["value"]],
|
|
61
|
+
"PackedFloat32Array": [["value"]],
|
|
62
|
+
"PackedFloat64Array": [["value"]],
|
|
63
|
+
"PackedStringArray": [["value"]],
|
|
64
|
+
"PackedVector2Array": [["value"]],
|
|
65
|
+
"PackedVector3Array": [["value"]],
|
|
66
|
+
"PackedVector4Array": [["value"]],
|
|
67
|
+
"PackedColorArray": [["value"]],
|
|
47
68
|
"float": [["value"]],
|
|
48
69
|
"int": [["value"]],
|
|
49
70
|
"RID": [["id"]],
|
|
@@ -79,6 +100,10 @@ static func written_as(type: int) -> String:
|
|
|
79
100
|
var alternatives: Array = REQUIRED.get(tag, [])
|
|
80
101
|
if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
|
|
81
102
|
return ""
|
|
103
|
+
# A packed list is written as the plain list wherever its type is declared, so its tag is no
|
|
104
|
+
# remedy there.
|
|
105
|
+
if PACKED_ELEMENTS.has(type) and type != TYPE_PACKED_BYTE_ARRAY:
|
|
106
|
+
return ""
|
|
82
107
|
var keys: Array = alternatives[0]
|
|
83
108
|
var optional: Array = OPTIONAL.get(tag, [])
|
|
84
109
|
var parts: Array[String] = []
|
|
@@ -172,6 +197,20 @@ static func fraction(value: Variant, type: int, elements: Array = []) -> bool:
|
|
|
172
197
|
return false
|
|
173
198
|
|
|
174
199
|
|
|
200
|
+
## A packed array tagged with its type, {"_type": "PackedStringArray", "value": [...]}, for where
|
|
201
|
+
## nothing declares what a plain list is to become: a setting the engine does not declare, or a
|
|
202
|
+
## Variant property, takes a list as an Array. Each item is fitted as a declared list's would be,
|
|
203
|
+
## and a list with an item that does not fit stays the tag it arrived as, for the caller to refuse.
|
|
204
|
+
static func packed_tagged(fields: Dictionary, fit: Callable) -> Variant:
|
|
205
|
+
var tag: String = str(fields["_type"])
|
|
206
|
+
if not buildable(fields, tag) or not fields["value"] is Array:
|
|
207
|
+
return fields
|
|
208
|
+
var type: int = LIST_TAGS[tag]
|
|
209
|
+
var items: Array = fields["value"]
|
|
210
|
+
var built: Variant = packed_from(items, type, fit)
|
|
211
|
+
return built if typeof(built) == type else fields
|
|
212
|
+
|
|
213
|
+
|
|
175
214
|
## The packed array [param type] built from [param items], each fitted by [param fit] to what the
|
|
176
215
|
## array holds, or [param items] as they are when one cannot become that, for the caller to refuse.
|
|
177
216
|
## Element by element, so a point given as its fields is read as one: converted whole, a list of
|
|
@@ -48,14 +48,22 @@ const EXPECTED: Dictionary[String, String] = {
|
|
|
48
48
|
}
|
|
49
49
|
## The tool name gdUnit4 reads its options after.
|
|
50
50
|
const TOOL: String = "GdUnitCmdTool.gd"
|
|
51
|
+
## Ends the runner's own arguments: everything after it is gdUnit4's options, for a run with no
|
|
52
|
+
## script path on its command line to find them after.
|
|
53
|
+
const GDUNIT: String = "--gdh-gdunit"
|
|
51
54
|
## How long a windowless engine sleeps after each frame. The engine's own default is the
|
|
52
55
|
## low-processor pause, 6.8 ms measured, which a run of suites awaiting frames spends thousands of
|
|
53
56
|
## times; zero makes a frame's delta round to nothing, and a tween then never moves.
|
|
54
57
|
const FRAME_SLEEP_USEC: int = 1000
|
|
58
|
+
## What the engine's shutdown prints as it clears the newest script and the oldest, on an engine
|
|
59
|
+
## whose walk [GdhShutdownWalk] guards. Kept in step with the server's engine-leaks.ts, which reads
|
|
60
|
+
## a run printing the first and not the second as a walk that stopped part way.
|
|
61
|
+
const WALK_BEGAN: String = "gdharness: the engine's shutdown began clearing scripts"
|
|
62
|
+
const WALK_ENDED: String = "gdharness: the engine's shutdown cleared every script"
|
|
63
|
+
## The meta of the theme database holding the scripts [method GdhShutdownWalk.hold] keeps.
|
|
64
|
+
const WALK_HOLD: StringName = &"gdharness_scripts_held_past_the_shutdown_walk"
|
|
55
65
|
|
|
56
66
|
var _runner: GdhRunner = null
|
|
57
|
-
## The suites of the run, compiled by [method compiled_together] and held for as long as it runs.
|
|
58
|
-
var _together: GDScript = null
|
|
59
67
|
|
|
60
68
|
|
|
61
69
|
## gdUnit4's runner reading its options after this script's path rather than after its own tool's,
|
|
@@ -64,12 +72,38 @@ class GdhRunner:
|
|
|
64
72
|
extends GdUnitTestCIRunner
|
|
65
73
|
|
|
66
74
|
var own_path: String = ""
|
|
75
|
+
## gdUnit4's options when they were given after [constant GDUNIT] rather than read from the
|
|
76
|
+
## engine's command line, as the suites scene gives them: a played scene has no script path to
|
|
77
|
+
## find them after.
|
|
78
|
+
var given: PackedStringArray = []
|
|
79
|
+
## The suites compiled by [method compiled_together], held for as long as the run goes on.
|
|
80
|
+
var together: GDScript = null
|
|
67
81
|
## The cases dealt by their rows, by `res://suite.gd:test_case`.
|
|
68
82
|
var row_shares: Dictionary[String, GdhRowShare] = {}
|
|
69
83
|
## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups against.
|
|
70
84
|
var cases_ended: int = 0
|
|
85
|
+
## The marker of the shutdown walk's oldest end, made as the run starts.
|
|
86
|
+
var walk_scripts: Array[GDScript] = []
|
|
87
|
+
|
|
88
|
+
## As the run ends, the marker of the walk's newest end, and every script held past the walk, on
|
|
89
|
+
## an engine whose walk can stop part way ([GdhShutdownWalk]).
|
|
90
|
+
func _notification(what: int) -> void:
|
|
91
|
+
if what != NOTIFICATION_EXIT_TREE or not GdhShutdownWalk.guarded():
|
|
92
|
+
return
|
|
93
|
+
var also: Array[GDScript] = walk_scripts.duplicate()
|
|
94
|
+
also.append_array(GdhShutdownWalk.marker(WALK_BEGAN))
|
|
95
|
+
if together != null:
|
|
96
|
+
also.append(together)
|
|
97
|
+
var own: GDScript = ResourceLoader.get_cached_ref(own_path) as GDScript
|
|
98
|
+
if own != null:
|
|
99
|
+
also.append(own)
|
|
100
|
+
GdhShutdownWalk.hold(also)
|
|
71
101
|
|
|
72
102
|
func get_cmdline_args() -> PackedStringArray:
|
|
103
|
+
if not given.is_empty():
|
|
104
|
+
var options: PackedStringArray = [TOOL]
|
|
105
|
+
options.append_array(given)
|
|
106
|
+
return options
|
|
73
107
|
return options_of(OS.get_cmdline_args(), own_path)
|
|
74
108
|
|
|
75
109
|
## gdUnit4 asks this of every case it finds, a parameterised case once a row, each row carrying
|
|
@@ -200,9 +234,92 @@ class GdhSessionCheck:
|
|
|
200
234
|
)
|
|
201
235
|
|
|
202
236
|
|
|
237
|
+
## The engine's shutdown walk over its scripts, on an engine below 4.8. It clears them from the
|
|
238
|
+
## newest, and a script whose release frees the next one, as the last reference to its base can,
|
|
239
|
+
## leaves the walk on freed memory, where it ends (godot#117991, fixed in 4.8 by godot#120976).
|
|
240
|
+
## Every script it never reached keeps what it holds, which the engine then reports as leaked at
|
|
241
|
+
## exit, so a run of suites that leaked nothing failed on it.
|
|
242
|
+
class GdhShutdownWalk:
|
|
243
|
+
extends RefCounted
|
|
244
|
+
|
|
245
|
+
## Whether the engine of [param version] is one whose walk can stop part way.
|
|
246
|
+
static func guarded(version: Dictionary = Engine.get_version_info()) -> bool:
|
|
247
|
+
var major: int = version.get("major", 0)
|
|
248
|
+
var minor: int = version.get("minor", 0)
|
|
249
|
+
return major == 4 and minor < 8
|
|
250
|
+
|
|
251
|
+
## A script the walk clears as it passes it, printing [param said] then, made now so it takes
|
|
252
|
+
## this place in the walk. It holds an instance of itself, which keeps it alive past the engine
|
|
253
|
+
## dropping its cache of scripts until the walk clears it, and the instance that prints, whose
|
|
254
|
+
## script is older and so is not cleared yet.
|
|
255
|
+
static func marker(said: String) -> Array[GDScript]:
|
|
256
|
+
var printing: GDScript = GDScript.new()
|
|
257
|
+
printing.source_code = (
|
|
258
|
+
'extends RefCounted\n\nvar said: String = ""\n\n\nfunc _notification(what: int) -> void:\n'
|
|
259
|
+
+ "\tif what == NOTIFICATION_PREDELETE:\n\t\tprint(said)\n"
|
|
260
|
+
)
|
|
261
|
+
var marking: GDScript = GDScript.new()
|
|
262
|
+
marking.source_code = (
|
|
263
|
+
"extends RefCounted\n\nstatic var kept: RefCounted = null\n"
|
|
264
|
+
+ "static var printing: RefCounted = null\n"
|
|
265
|
+
)
|
|
266
|
+
if printing.reload() != OK or marking.reload() != OK:
|
|
267
|
+
push_error("gdharness test could not make the marker of the engine's shutdown: " + said)
|
|
268
|
+
return []
|
|
269
|
+
var instance: Object = printing.new()
|
|
270
|
+
instance.set("said", said)
|
|
271
|
+
marking.set("kept", marking.new())
|
|
272
|
+
marking.set("printing", instance)
|
|
273
|
+
return [printing, marking]
|
|
274
|
+
|
|
275
|
+
## Every script loaded from a file under res://, and [param also], held until the theme database
|
|
276
|
+
## goes, which is after the walk and before the GDScript language does. Freed in the walk, a
|
|
277
|
+
## script can take the next one with it and end the walk; held, none is freed there, so the walk
|
|
278
|
+
## reaches every script and what the engine reports at exit is only what was leaked. A script a
|
|
279
|
+
## run leaks is held by what leaked it as well, and is still reported.
|
|
280
|
+
static func hold(also: Array[GDScript]) -> void:
|
|
281
|
+
var held: Array[GDScript] = also.duplicate()
|
|
282
|
+
var pending: PackedStringArray = ["res://"]
|
|
283
|
+
while not pending.is_empty():
|
|
284
|
+
var directory: String = pending[pending.size() - 1]
|
|
285
|
+
pending.remove_at(pending.size() - 1)
|
|
286
|
+
for file: String in DirAccess.get_files_at(directory):
|
|
287
|
+
var path: String = directory.path_join(file)
|
|
288
|
+
if file.ends_with(".gd") and ResourceLoader.has_cached(path):
|
|
289
|
+
var script: GDScript = ResourceLoader.get_cached_ref(path) as GDScript
|
|
290
|
+
if script != null:
|
|
291
|
+
held.append(script)
|
|
292
|
+
for inner: String in DirAccess.get_directories_at(directory):
|
|
293
|
+
if not inner.begins_with("."):
|
|
294
|
+
var _inner: bool = pending.append(directory.path_join(inner))
|
|
295
|
+
ThemeDB.set_meta(WALK_HOLD, held)
|
|
296
|
+
|
|
297
|
+
|
|
203
298
|
func _initialize() -> void:
|
|
299
|
+
_runner = started(OS.get_cmdline_user_args())
|
|
300
|
+
_runner.own_path = get_script().resource_path
|
|
301
|
+
root.add_child(_runner)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
## As gdUnit4's own `GdUnitCmdTool.gd` ends, which names in a verbose run the nodes left orphaned.
|
|
305
|
+
func _finalize() -> void:
|
|
306
|
+
queue_delete(_runner)
|
|
307
|
+
if OS.is_stdout_verbose():
|
|
308
|
+
print("Orphan nodes left as the run ends:")
|
|
309
|
+
Window.print_orphan_nodes()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
## The runner over the suites [param given] lists, to be added to the tree, which starts it. Shared
|
|
313
|
+
## by this script and the suites scene of the tests addon, which loads this file from where the
|
|
314
|
+
## server ships it and calls this, since the addon itself names no gdUnit4 class.
|
|
315
|
+
static func started(given: PackedStringArray) -> GdhRunner:
|
|
316
|
+
# First, so the marker is older than every script the run loads.
|
|
317
|
+
var oldest: Array[GDScript] = []
|
|
318
|
+
if GdhShutdownWalk.guarded():
|
|
319
|
+
oldest = GdhShutdownWalk.marker(WALK_ENDED)
|
|
204
320
|
OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
|
|
205
|
-
var
|
|
321
|
+
var at: int = given.find(GDUNIT)
|
|
322
|
+
var asked: PackedStringArray = given if at < 0 else given.slice(0, at)
|
|
206
323
|
var unread: PackedStringArray = []
|
|
207
324
|
for flag: String in REPORTS_OFF:
|
|
208
325
|
if asked.has(flag):
|
|
@@ -221,28 +338,22 @@ func _initialize() -> void:
|
|
|
221
338
|
)
|
|
222
339
|
)
|
|
223
340
|
var suites: PackedStringArray = listed(asked, LISTED)
|
|
341
|
+
var runner: GdhRunner = GdhRunner.new()
|
|
342
|
+
runner.walk_scripts = oldest
|
|
343
|
+
if at >= 0:
|
|
344
|
+
runner.given = given.slice(at + 1)
|
|
224
345
|
if asked.has(TOGETHER):
|
|
225
|
-
|
|
346
|
+
runner.together = compiled_together(suites)
|
|
226
347
|
var expected_out: String = argument_after(asked, EXPECTED_OUT)
|
|
227
348
|
if not expected_out.is_empty():
|
|
228
349
|
write_expected(expected_out, suites)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
_runner.skip_test_suites(listed(asked, SKIPPING))
|
|
233
|
-
_runner.row_shares = row_shares_of(listed(asked, ROWS))
|
|
350
|
+
runner.add_test_suites(suites)
|
|
351
|
+
runner.skip_test_suites(listed(asked, SKIPPING))
|
|
352
|
+
runner.row_shares = row_shares_of(listed(asked, ROWS))
|
|
234
353
|
# Added to the list itself rather than through `register`, which saves every hook it holds into
|
|
235
354
|
# `project.godot`.
|
|
236
|
-
hooks.enigne_hooks.append(GdhSessionCheck.new(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
## As gdUnit4's own `GdUnitCmdTool.gd` ends, which names in a verbose run the nodes left orphaned.
|
|
241
|
-
func _finalize() -> void:
|
|
242
|
-
queue_delete(_runner)
|
|
243
|
-
if OS.is_stdout_verbose():
|
|
244
|
-
print("Orphan nodes left as the run ends:")
|
|
245
|
-
Window.print_orphan_nodes()
|
|
355
|
+
hooks.enigne_hooks.append(GdhSessionCheck.new(runner))
|
|
356
|
+
return runner
|
|
246
357
|
|
|
247
358
|
|
|
248
359
|
## Turns off the hooks among [param hooks] whose scripts [param unread] names, answering the scripts
|