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.
Files changed (33) hide show
  1. package/build/cli.js +19152 -17965
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +49 -0
  3. package/build/godot/addons/gdharness_editor/file_walk.gd +7 -4
  4. package/build/godot/addons/gdharness_editor/serialisation.gd +2 -0
  5. package/build/godot/addons/gdharness_editor/value_shapes.gd +39 -0
  6. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +19 -0
  7. package/build/godot/addons/gdharness_runtime/serialisation.gd +2 -0
  8. package/build/godot/addons/gdharness_runtime/value_shapes.gd +39 -0
  9. package/build/godot/addons/gdharness_tests/bench.gd +28 -0
  10. package/build/godot/addons/gdharness_tests/bench.gd.uid +1 -0
  11. package/build/godot/addons/gdharness_tests/event.gd +92 -0
  12. package/build/godot/addons/gdharness_tests/event.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_tests/share.gd +289 -0
  14. package/build/godot/addons/gdharness_tests/share.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_tests/suites.gd +34 -0
  16. package/build/godot/addons/gdharness_tests/suites.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_tests/suites.tscn +6 -0
  18. package/build/godot/addons/gdharness_tests/tweens.gd +24 -0
  19. package/build/godot/addons/gdharness_tests/tweens.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_tests/wait.gd +53 -0
  21. package/build/godot/addons/gdharness_tests/wait.gd.uid +1 -0
  22. package/build/godot/loader/loader.gd +28 -0
  23. package/build/godot/operations/class_cache.gd +1 -1
  24. package/build/godot/operations/file_walk.gd +7 -4
  25. package/build/godot/operations/input_actions.gd +2 -2
  26. package/build/godot/operations/plugins.gd +2 -2
  27. package/build/godot/operations/project_config.gd +37 -10
  28. package/build/godot/operations/project_file.gd +89 -31
  29. package/build/godot/operations/serialisation.gd +2 -0
  30. package/build/godot/operations/value_shapes.gd +39 -0
  31. package/build/godot/tests/listed.gd +196 -20
  32. package/build/keeper.js +8398 -911
  33. package/package.json +1 -1
@@ -19,6 +19,9 @@ const DIGEST_MARKER: String = "res://addons/gdharness_editor/.gdharness-digest"
19
19
  ## step with `announcementPath` in src/bridge-announce.ts.
20
20
  const ANNOUNCEMENT: String = "res://.godot/gdharness-bridge.json"
21
21
  const ANNOUNCE_PROTOCOL: int = 1
22
+ ## Where each editor of this project says which ports it serves; see [method _ports_record].
23
+ const EDITORS_RECORDED: String = "res://.godot/gdharness-editors"
24
+ const EDITORS_RECORDED_PROTOCOL: int = 1
22
25
  const RECONNECT_DELAY: float = 3.0
23
26
  const MAX_RECONNECT_DELAY: float = 30.0
24
27
 
@@ -128,6 +131,52 @@ func _ready() -> void:
128
131
 
129
132
  set_process(true)
130
133
  _initialized = true
134
+ _record_ports()
135
+
136
+
137
+ func _exit_tree() -> void:
138
+ var record: String = _ports_record()
139
+ if FileAccess.file_exists(record):
140
+ var _removed: Error = DirAccess.remove_absolute(record)
141
+
142
+
143
+ ## The file saying which ports this editor serves, named by its process id, so `gdharness lsp` can
144
+ ## find its language server when a server moved it off the default. One for each process rather
145
+ ## than one for the project, because a gate's `--import` loads this addon too, and its plugin is
146
+ ## shown none of the options that would say so; the reader tells the two apart by command line.
147
+ ## Kept in step with `editorRecordsIn` in src/lsp-bridge.ts.
148
+ static func _ports_record() -> String:
149
+ return ProjectSettings.globalize_path(EDITORS_RECORDED).path_join("%d.json" % OS.get_process_id())
150
+
151
+
152
+ func _record_ports() -> void:
153
+ if not Engine.is_editor_hint():
154
+ return
155
+ var made: Error = DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(EDITORS_RECORDED))
156
+ if made != OK and made != ERR_ALREADY_EXISTS:
157
+ return
158
+ var file: FileAccess = FileAccess.open(_ports_record(), FileAccess.WRITE)
159
+ if file == null:
160
+ return
161
+ var written: bool = (
162
+ file
163
+ . store_string(
164
+ (
165
+ JSON
166
+ . stringify(
167
+ {
168
+ "protocol": EDITORS_RECORDED_PROTOCOL,
169
+ "pid": OS.get_process_id(),
170
+ "lsp_port": _serves(LSP_ASKED, LSP_SETTING),
171
+ "dap_port": _serves(DAP_ASKED, DAP_SETTING),
172
+ "written_at_ms": int(Time.get_unix_time_from_system() * 1000.0),
173
+ }
174
+ )
175
+ )
176
+ )
177
+ )
178
+ if not written:
179
+ push_warning("gdharness: could not write " + _ports_record())
131
180
 
132
181
 
133
182
  func _process(_delta: float) -> void:
@@ -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
- func find_files(path: String, extension: String) -> Array[String]:
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
 
@@ -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
@@ -71,6 +71,10 @@ const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
71
71
  ## not served. Kept in step with `QUIET_SCENES_SETTING` in src/setup.ts.
72
72
  const QUIET_SCENES_SETTING: String = "gdharness/runtime/quiet_scenes_under"
73
73
 
74
+ ## What every user argument `gdharness spread` hands its engines starts with. Kept in step with
75
+ ## the constants of GdhShare in addons/gdharness_tests/share.gd.
76
+ const SPREAD_ARGUMENTS: String = "--gdh-share"
77
+
74
78
  ## What the engine takes a bare argument for as the scene to run, by its extension.
75
79
  const SCENE_EXTENSIONS: PackedStringArray = ["tscn", "scn", "escn", "res", "tres"]
76
80
 
@@ -489,6 +493,12 @@ func _start_server() -> void:
489
493
  _enabled = false
490
494
  return
491
495
 
496
+ # Nor an engine `gdharness spread` started, whatever folder its scene is in: its workers and the
497
+ # engine gathering their outcomes play a bench rather than the game, several at once.
498
+ if spread_engine(OS.get_cmdline_user_args()):
499
+ _enabled = false
500
+ return
501
+
492
502
  _server = TCPServer.new()
493
503
  # listen() defaults bind_address to "*", which exposes the game to the whole network.
494
504
  var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
@@ -540,6 +550,15 @@ static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> Strin
540
550
  return ""
541
551
 
542
552
 
553
+ ## Whether [param given], the engine's user arguments, are those `gdharness spread` hands its
554
+ ## engines, which the tests addon's GdhShare reads.
555
+ static func spread_engine(given: PackedStringArray) -> bool:
556
+ for argument: String in given:
557
+ if argument.begins_with(SPREAD_ARGUMENTS):
558
+ return true
559
+ return false
560
+
561
+
543
562
  ## Whether the engine was told to run a script rather than the game.
544
563
  func _script_run() -> bool:
545
564
  var given: PackedStringArray = OS.get_cmdline_args()
@@ -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
@@ -0,0 +1,28 @@
1
+ class_name GdhBench
2
+ extends RefCounted
3
+
4
+ ## Work played a step a frame, for the script profiler. The engine sends the profiler what each
5
+ ## frame called and holds names for only so many functions a frame, so work played inside one frame
6
+ ## comes back with most of its functions unnamed; spread over frames, every step is named.
7
+ ##
8
+ ## func _ready() -> void:
9
+ ## var timed: Dictionary = await GdhBench.each_frame(get_tree(), 400, _run.advance)
10
+ ## print("bench: %d days, %.2f ms a day" % [timed["steps"], timed["ms_each"]])
11
+ ## get_tree().quit()
12
+
13
+
14
+ ## Calls [param step] once a frame, up to [param most] times, and stops early when it returns
15
+ ## false. Answers how many steps ran under "steps", and under "ms_each" the wall-clock milliseconds a
16
+ ## step took on average, the step alone and not the frame around it.
17
+ static func each_frame(tree: SceneTree, most: int, step: Callable) -> Dictionary:
18
+ var steps: int = 0
19
+ var spent_usec: int = 0
20
+ while steps < most:
21
+ await tree.process_frame
22
+ var began: int = Time.get_ticks_usec()
23
+ var going: Variant = step.call()
24
+ spent_usec += Time.get_ticks_usec() - began
25
+ steps += 1
26
+ if going is bool and going == false:
27
+ break
28
+ return {"steps": steps, "ms_each": spent_usec / 1000.0 / maxf(1.0, float(steps))}
@@ -0,0 +1 @@
1
+ uid://dk4ffqm2xbsn7
@@ -0,0 +1,92 @@
1
+ class_name GdhEvent
2
+ extends RefCounted
3
+ ## Input events built in one line, for a case that hands them to a control or to the engine.
4
+ ##
5
+ ## A position is the point in the window, which is where `Input.parse_input_event` delivers the
6
+ ## event and what a control's own `_gui_input` reads when it is handed the event directly. The
7
+ ## global position is set to the same point, as the window's own events carry it, for code that
8
+ ## reads that field instead.
9
+
10
+
11
+ ## [param button] pressed at [param at], or released when [param pressed] is false.
12
+ static func click(
13
+ at: Vector2, button: MouseButton = MOUSE_BUTTON_LEFT, pressed: bool = true
14
+ ) -> InputEventMouseButton:
15
+ var made: InputEventMouseButton = InputEventMouseButton.new()
16
+ made.button_index = button
17
+ made.pressed = pressed
18
+ made.position = at
19
+ made.global_position = at
20
+ if pressed:
21
+ made.button_mask = _mask_of(button)
22
+ return made
23
+
24
+
25
+ ## The release of [param button] at [param at], which ends the press [method click] began.
26
+ static func release(at: Vector2, button: MouseButton = MOUSE_BUTTON_LEFT) -> InputEventMouseButton:
27
+ return click(at, button, false)
28
+
29
+
30
+ ## One turn of the wheel at [param at], up or down. The engine sends a wheel turn as a press.
31
+ static func wheel(at: Vector2, up: bool = true) -> InputEventMouseButton:
32
+ return click(at, MOUSE_BUTTON_WHEEL_UP if up else MOUSE_BUTTON_WHEEL_DOWN)
33
+
34
+
35
+ ## The mouse moved to [param at] by [param moved], holding [param held], a mask of buttons such
36
+ ## as MOUSE_BUTTON_MASK_LEFT for a drag.
37
+ static func motion(at: Vector2, moved: Vector2 = Vector2.ZERO, held: int = 0) -> InputEventMouseMotion:
38
+ var made: InputEventMouseMotion = InputEventMouseMotion.new()
39
+ made.position = at
40
+ made.global_position = at
41
+ made.relative = moved
42
+ made.button_mask = held
43
+ return made
44
+
45
+
46
+ ## [param key_pressed] pressed, or released when [param pressed] is false. The key is set as both the
47
+ ## keycode and the physical keycode, so a binding to either matches it.
48
+ static func key(key_pressed: Key, pressed: bool = true) -> InputEventKey:
49
+ var made: InputEventKey = InputEventKey.new()
50
+ made.keycode = key_pressed
51
+ made.physical_keycode = key_pressed
52
+ made.pressed = pressed
53
+ return made
54
+
55
+
56
+ ## [param button] of the pad [param device] pressed, or released when [param pressed] is false.
57
+ static func pad(button: JoyButton, pressed: bool = true, device: int = 0) -> InputEventJoypadButton:
58
+ var made: InputEventJoypadButton = InputEventJoypadButton.new()
59
+ made.button_index = button
60
+ made.pressed = pressed
61
+ made.pressure = 1.0 if pressed else 0.0
62
+ made.device = device
63
+ return made
64
+
65
+
66
+ ## [param axis_moved] of the pad [param device] at [param value], from -1 to 1.
67
+ static func axis(axis_moved: JoyAxis, value: float, device: int = 0) -> InputEventJoypadMotion:
68
+ var made: InputEventJoypadMotion = InputEventJoypadMotion.new()
69
+ made.axis = axis_moved
70
+ made.axis_value = value
71
+ made.device = device
72
+ return made
73
+
74
+
75
+ ## The action [param action_named] pressed at [param strength], or released when [param pressed] is false.
76
+ static func action(action_named: StringName, pressed: bool = true, strength: float = 1.0) -> InputEventAction:
77
+ var made: InputEventAction = InputEventAction.new()
78
+ made.action = action_named
79
+ made.pressed = pressed
80
+ made.strength = strength if pressed else 0.0
81
+ return made
82
+
83
+
84
+ static func _mask_of(button: MouseButton) -> int:
85
+ match button:
86
+ MOUSE_BUTTON_LEFT:
87
+ return MOUSE_BUTTON_MASK_LEFT
88
+ MOUSE_BUTTON_RIGHT:
89
+ return MOUSE_BUTTON_MASK_RIGHT
90
+ MOUSE_BUTTON_MIDDLE:
91
+ return MOUSE_BUTTON_MASK_MIDDLE
92
+ return 0
@@ -0,0 +1 @@
1
+ uid://cmwm44sgxytkj