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
|
@@ -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,10 +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] = {}
|
|
83
|
+
## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups against.
|
|
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)
|
|
69
101
|
|
|
70
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
|
|
71
107
|
return options_of(OS.get_cmdline_args(), own_path)
|
|
72
108
|
|
|
73
109
|
## gdUnit4 asks this of every case it finds, a parameterised case once a row, each row carrying
|
|
@@ -87,6 +123,8 @@ class GdhRunner:
|
|
|
87
123
|
func _on_gdunit_event(event: GdUnitEvent) -> void:
|
|
88
124
|
if event.type() == GdUnitEvent.SESSION_START:
|
|
89
125
|
_test_session = GdhIndexedSession.of(_test_session)
|
|
126
|
+
elif event.type() == GdUnitEvent.TESTCASE_AFTER:
|
|
127
|
+
cases_ended += 1
|
|
90
128
|
super(event)
|
|
91
129
|
|
|
92
130
|
## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
|
|
@@ -130,6 +168,8 @@ class GdhRowShare:
|
|
|
130
168
|
class GdhIndexedSession:
|
|
131
169
|
extends GdUnitTestSession
|
|
132
170
|
|
|
171
|
+
## The cases found by id, which [GdhSessionCheck] reads to tell that the console used this session.
|
|
172
|
+
var lookups: int = 0
|
|
133
173
|
var _by_id: Dictionary[String, GdUnitTestCase] = {}
|
|
134
174
|
|
|
135
175
|
func _init(cases: Array[GdUnitTestCase], reports_in: String) -> void:
|
|
@@ -138,6 +178,7 @@ class GdhIndexedSession:
|
|
|
138
178
|
_by_id[str(test.guid)] = test
|
|
139
179
|
|
|
140
180
|
func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
|
|
181
|
+
lookups += 1
|
|
141
182
|
var found: GdUnitTestCase = _by_id.get(str(id), null)
|
|
142
183
|
return found
|
|
143
184
|
|
|
@@ -149,26 +190,114 @@ class GdhIndexedSession:
|
|
|
149
190
|
return GdhIndexedSession.new(made._test_cases, made.report_path)
|
|
150
191
|
|
|
151
192
|
|
|
193
|
+
## A session hook of the runner's own, failing the run when the [GdhIndexedSession] is not the
|
|
194
|
+
## session gdUnit4 works from. The swap holds only while gdUnit4 announces a session before it hands
|
|
195
|
+
## it to the hooks and the console, and a gdUnit4 that changed that order would otherwise run every
|
|
196
|
+
## suite as before, only slower, with nothing said. Checked in the gdUnit4 the game installed, on
|
|
197
|
+
## every run, rather than in the one gdharness tests against.
|
|
198
|
+
class GdhSessionCheck:
|
|
199
|
+
extends GdUnitTestSessionHook
|
|
200
|
+
|
|
201
|
+
var _runner: GdhRunner = null
|
|
202
|
+
|
|
203
|
+
func _init(runner: GdhRunner) -> void:
|
|
204
|
+
super("gdharness session check", "Fails the run when gdUnit4 works from another session.")
|
|
205
|
+
_runner = runner
|
|
206
|
+
|
|
207
|
+
func startup(session: GdUnitTestSession) -> GdUnitResult:
|
|
208
|
+
if session is GdhIndexedSession:
|
|
209
|
+
return GdUnitResult.success()
|
|
210
|
+
return GdUnitResult.error(
|
|
211
|
+
(
|
|
212
|
+
(
|
|
213
|
+
"gdharness test: gdUnit4 %s handed its session hooks the session before announcing it, so"
|
|
214
|
+
+ " the runner's indexed session is not the one they read. This gdUnit4 starts a session"
|
|
215
|
+
+ " differently from the ones gdharness knows; report it to gdharness with the version."
|
|
216
|
+
)
|
|
217
|
+
% GdUnit4Version.current()
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
func shutdown(session: GdUnitTestSession) -> GdUnitResult:
|
|
222
|
+
var indexed: GdhIndexedSession = session as GdhIndexedSession
|
|
223
|
+
if indexed == null or _runner.cases_ended == 0 or indexed.lookups > 0:
|
|
224
|
+
return GdUnitResult.success()
|
|
225
|
+
return GdUnitResult.error(
|
|
226
|
+
(
|
|
227
|
+
(
|
|
228
|
+
"gdharness test: gdUnit4 %s ran %d cases without looking one up in the runner's indexed"
|
|
229
|
+
+ " session, so its console reads another session. This gdUnit4 starts a session"
|
|
230
|
+
+ " differently from the ones gdharness knows; report it to gdharness with the version."
|
|
231
|
+
)
|
|
232
|
+
% [GdUnit4Version.current(), _runner.cases_ended]
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
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
|
+
|
|
152
298
|
func _initialize() -> void:
|
|
153
|
-
|
|
154
|
-
var asked: PackedStringArray = OS.get_cmdline_user_args()
|
|
155
|
-
var unread: PackedStringArray = []
|
|
156
|
-
for flag: String in REPORTS_OFF:
|
|
157
|
-
if asked.has(flag):
|
|
158
|
-
var _unread: bool = unread.append(REPORTS_OFF[flag])
|
|
159
|
-
if not unread.is_empty():
|
|
160
|
-
var _off: PackedStringArray = unread_off(GdUnitTestSessionHookService.instance(), unread)
|
|
161
|
-
var suites: PackedStringArray = listed(asked, LISTED)
|
|
162
|
-
if asked.has(TOGETHER):
|
|
163
|
-
_together = compiled_together(suites)
|
|
164
|
-
var expected_out: String = argument_after(asked, EXPECTED_OUT)
|
|
165
|
-
if not expected_out.is_empty():
|
|
166
|
-
write_expected(expected_out, suites)
|
|
167
|
-
_runner = GdhRunner.new()
|
|
299
|
+
_runner = started(OS.get_cmdline_user_args())
|
|
168
300
|
_runner.own_path = get_script().resource_path
|
|
169
|
-
_runner.add_test_suites(suites)
|
|
170
|
-
_runner.skip_test_suites(listed(asked, SKIPPING))
|
|
171
|
-
_runner.row_shares = row_shares_of(listed(asked, ROWS))
|
|
172
301
|
root.add_child(_runner)
|
|
173
302
|
|
|
174
303
|
|
|
@@ -180,6 +309,53 @@ func _finalize() -> void:
|
|
|
180
309
|
Window.print_orphan_nodes()
|
|
181
310
|
|
|
182
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)
|
|
320
|
+
OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
|
|
321
|
+
var at: int = given.find(GDUNIT)
|
|
322
|
+
var asked: PackedStringArray = given if at < 0 else given.slice(0, at)
|
|
323
|
+
var unread: PackedStringArray = []
|
|
324
|
+
for flag: String in REPORTS_OFF:
|
|
325
|
+
if asked.has(flag):
|
|
326
|
+
var _unread: bool = unread.append(REPORTS_OFF[flag])
|
|
327
|
+
var hooks: GdUnitTestSessionHookService = GdUnitTestSessionHookService.instance()
|
|
328
|
+
var off: PackedStringArray = unread_off(hooks, unread)
|
|
329
|
+
for path: String in unread:
|
|
330
|
+
if not off.has(path):
|
|
331
|
+
push_error(
|
|
332
|
+
(
|
|
333
|
+
(
|
|
334
|
+
"gdharness test: gdUnit4 %s registers no session hook %s, so the report it writes is not"
|
|
335
|
+
+ " turned off; report it to gdharness with the version."
|
|
336
|
+
)
|
|
337
|
+
% [GdUnit4Version.current(), path]
|
|
338
|
+
)
|
|
339
|
+
)
|
|
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)
|
|
345
|
+
if asked.has(TOGETHER):
|
|
346
|
+
runner.together = compiled_together(suites)
|
|
347
|
+
var expected_out: String = argument_after(asked, EXPECTED_OUT)
|
|
348
|
+
if not expected_out.is_empty():
|
|
349
|
+
write_expected(expected_out, suites)
|
|
350
|
+
runner.add_test_suites(suites)
|
|
351
|
+
runner.skip_test_suites(listed(asked, SKIPPING))
|
|
352
|
+
runner.row_shares = row_shares_of(listed(asked, ROWS))
|
|
353
|
+
# Added to the list itself rather than through `register`, which saves every hook it holds into
|
|
354
|
+
# `project.godot`.
|
|
355
|
+
hooks.enigne_hooks.append(GdhSessionCheck.new(runner))
|
|
356
|
+
return runner
|
|
357
|
+
|
|
358
|
+
|
|
183
359
|
## Turns off the hooks among [param hooks] whose scripts [param unread] names, answering the scripts
|
|
184
360
|
## of those it turned off. Through the mark gdUnit4's own `is_enabled` reads, rather than its
|
|
185
361
|
## `enable_hook`, which writes the setting into `project.godot`: a run changes nothing in the tree.
|