gdharness 1.3.41 → 1.3.43

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 (107) hide show
  1. package/build/cli.js +131 -47
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +84 -35
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +114 -39
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +94 -41
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +37 -12
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +136 -79
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +41 -28
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +84 -50
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +181 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +75 -43
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +66 -0
  33. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +171 -62
  35. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  36. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  37. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  38. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  39. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  40. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  41. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  42. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  43. package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
  44. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  45. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +153 -148
  46. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  47. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  48. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  49. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  50. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  51. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  52. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  53. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +38 -20
  54. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  55. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  56. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  57. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  58. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  59. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  60. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  61. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  62. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  63. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  64. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  65. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  66. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  67. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  68. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  69. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  70. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  71. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  72. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  73. package/build/godot/loader/loader.gd +9 -9
  74. package/build/godot/operations/animation_reads.gd +84 -35
  75. package/build/godot/operations/audio_buses.gd +48 -19
  76. package/build/godot/operations/class_cache.gd +16 -11
  77. package/build/godot/operations/classdb_queries.gd +89 -44
  78. package/build/godot/operations/declared_values.gd +68 -32
  79. package/build/godot/operations/dependencies.gd +139 -93
  80. package/build/godot/operations/editor_tools.gd +9 -10
  81. package/build/godot/operations/export_presets.gd +100 -54
  82. package/build/godot/operations/file_reads.gd +114 -39
  83. package/build/godot/operations/file_walk.gd +12 -5
  84. package/build/godot/operations/gdscript_analysis.gd +105 -66
  85. package/build/godot/operations/gdscript_authoring.gd +138 -39
  86. package/build/godot/operations/godot_operations.gd +17 -11
  87. package/build/godot/operations/import_pipeline.gd +110 -59
  88. package/build/godot/operations/input_actions.gd +63 -39
  89. package/build/godot/operations/patterns.gd +3 -1
  90. package/build/godot/operations/plugins.gd +36 -16
  91. package/build/godot/operations/project_config.gd +144 -27
  92. package/build/godot/operations/project_diagnostics.gd +24 -11
  93. package/build/godot/operations/project_file.gd +16 -16
  94. package/build/godot/operations/project_validation.gd +71 -44
  95. package/build/godot/operations/property_paths.gd +22 -12
  96. package/build/godot/operations/property_values.gd +143 -78
  97. package/build/godot/operations/reading.gd +37 -12
  98. package/build/godot/operations/rename_checks.gd +2 -2
  99. package/build/godot/operations/resource_files.gd +11 -9
  100. package/build/godot/operations/same_values.gd +4 -4
  101. package/build/godot/operations/scene_connections.gd +27 -13
  102. package/build/godot/operations/script_check.gd +52 -49
  103. package/build/godot/operations/serialisation.gd +75 -51
  104. package/build/godot/operations/setting_reads.gd +7 -6
  105. package/build/godot/operations/value_shapes.gd +5 -6
  106. package/build/godot/tests/listed.gd +321 -268
  107. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  extends SceneTree
2
-
3
2
  ## gdUnit4's command-line runner over the suites a file lists, in the file's order. One `-a` a suite
4
3
  ## outgrows the command line once a game has a few hundred suites (Windows stops at 32767
5
4
  ## characters), so the suites come from the file named after `--` with [constant LISTED], and the
@@ -26,7 +25,8 @@ const ROWS: String = "--rows-in"
26
25
  ## row an engine found to tell one that no engine ran.
27
26
  const LEFT_OUT: String = "gdharness left out"
28
27
  ## Printed before each row an entry of [constant SKIPPING] leaves out, as
29
- ## `res://suite.gd > test_case:row`, so the caller can tell an entry naming a row that does not exist.
28
+ ## `res://suite.gd > test_case:row`, so the caller can tell an entry naming a row that does not
29
+ ## exist.
30
30
  const IGNORED_ROW: String = "gdharness ignored"
31
31
  ## Printed after a case or a suite's hooks that gdUnit4 says left orphan nodes, as
32
32
  ## `res://suite.gd > test_case left 2: Node "Sandbox" of res://sandbox.gd, Timer "Timer"`, with
@@ -76,6 +76,249 @@ const WALK_HOLD: StringName = &"gdharness_scripts_held_past_the_shutdown_walk"
76
76
  var _runner: GdhRunner = null
77
77
 
78
78
 
79
+ func _initialize() -> void:
80
+ _runner = started(OS.get_cmdline_user_args())
81
+ _runner.own_path = get_script().resource_path
82
+ root.add_child(_runner)
83
+
84
+
85
+ ## As gdUnit4's own `GdUnitCmdTool.gd` ends, which names in a verbose run the nodes left orphaned.
86
+ func _finalize() -> void:
87
+ queue_delete(_runner)
88
+ if OS.is_stdout_verbose():
89
+ print("Orphan nodes left as the run ends:")
90
+ Window.print_orphan_nodes()
91
+
92
+
93
+ ## The runner over the suites [param given] lists, to be added to the tree, which starts it. Shared
94
+ ## by this script and the suites scene of the tests addon, which loads this file from where the
95
+ ## server ships it and calls this, since the addon itself names no gdUnit4 class.
96
+ static func started(given: PackedStringArray) -> GdhRunner:
97
+ # First, so the marker is older than every script the run loads.
98
+ var oldest: Array[GDScript] = []
99
+ if GdhShutdownWalk.guarded():
100
+ oldest = GdhShutdownWalk.marker(WALK_ENDED)
101
+ OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
102
+ var at: int = given.find(GDUNIT)
103
+ var asked: PackedStringArray = given if at < 0 else given.slice(0, at)
104
+ var unread: PackedStringArray = []
105
+ for flag: String in REPORTS_OFF:
106
+ if asked.has(flag):
107
+ var _unread: bool = unread.append(REPORTS_OFF[flag])
108
+ var hooks: GdUnitTestSessionHookService = GdUnitTestSessionHookService.instance()
109
+ var off: PackedStringArray = unread_off(hooks, unread)
110
+ for path: String in unread:
111
+ if not off.has(path):
112
+ push_error(
113
+ (
114
+ (
115
+ "gdharness test: gdUnit4 %s registers no session hook %s, so the report it "
116
+ + "writes is not turned off; report it to gdharness with the version."
117
+ )
118
+ % [GdUnit4Version.current(), path]
119
+ )
120
+ )
121
+ var suites: PackedStringArray = listed(asked, LISTED)
122
+ var runner: GdhRunner = GdhRunner.new()
123
+ runner.walk_scripts = oldest
124
+ if at >= 0:
125
+ runner.given = given.slice(at + 1)
126
+ if asked.has(TOGETHER):
127
+ runner.together = compiled_together(suites)
128
+ var expected_out: String = argument_after(asked, EXPECTED_OUT)
129
+ if not expected_out.is_empty():
130
+ write_expected(expected_out, suites)
131
+ runner.add_test_suites(suites)
132
+ for entry: String in listed(asked, SKIPPING):
133
+ if is_one_row(entry):
134
+ runner.left_out_rows[entry] = true
135
+ elif is_one_case(entry):
136
+ runner.left_out[entry] = true
137
+ else:
138
+ runner.skip_test_suite(entry)
139
+ runner.row_shares = row_shares_of(listed(asked, ROWS))
140
+ # Added to the list itself rather than through `register`, which saves every hook it holds into
141
+ # `project.godot`.
142
+ hooks.enigne_hooks.append(GdhSessionCheck.new(runner))
143
+ return runner
144
+
145
+
146
+ ## Turns off the hooks among [param hooks] whose scripts [param unread] names, answering the scripts
147
+ ## of those it turned off. Through the mark gdUnit4's own `is_enabled` reads, rather than its
148
+ ## `enable_hook`, which writes the setting into `project.godot`: a run changes nothing in the tree.
149
+ static func unread_off(
150
+ hooks: GdUnitTestSessionHookService, unread: PackedStringArray
151
+ ) -> PackedStringArray:
152
+ var off: PackedStringArray = []
153
+ for hook: GdUnitTestSessionHook in hooks.enigne_hooks:
154
+ var path: String = hook.get_script().resource_path
155
+ if unread.has(path):
156
+ hook.set_meta("enabled", false)
157
+ var _added: bool = off.append(path)
158
+ return off
159
+
160
+
161
+ ## [param suites] compiled inside one script, answered for the caller to hold so gdUnit4's own loads
162
+ ## find them compiled. The engine keeps what it read of a class only while the compile that asked
163
+ ## for it runs (`GDScriptCache`), so a suite compiled on its own reads again every class it reaches
164
+ ## a member of, some seventy milliseconds a suite. As the preloads of one script naming every class
165
+ ## of the game's own first, held from that script's compile, they share one reading of each: two
166
+ ## hundred suites compiled in a third of the time they took one at a time. The names are public,
167
+ ## since a private one unread is a warning and every warning an error, and each a variable, since a
168
+ ## class is no constant expression. A suite that does not compile fails here and again when gdUnit4
169
+ ## loads it, which reports it as it would have. A directory listed is left to gdUnit4 to walk.
170
+ static func compiled_together(suites: PackedStringArray) -> GDScript:
171
+ var lines: PackedStringArray = ["extends RefCounted"]
172
+ var at: int = 0
173
+ for named: String in held_classes():
174
+ var _held: bool = lines.append("var held_%d: Variant = %s" % [at, named])
175
+ at += 1
176
+ at = 0
177
+ for path: String in suites:
178
+ if not path.ends_with(".gd"):
179
+ continue
180
+ var _suite: bool = lines.append('var suite_%d: Variant = preload("%s")' % [at, path])
181
+ at += 1
182
+ var together: GDScript = GDScript.new()
183
+ together.source_code = "\n".join(lines) + "\n"
184
+ var _compiled: Error = together.reload()
185
+ return together
186
+
187
+
188
+ ## The classes [method compiled_together] names before the suites: the game's own GDScript classes,
189
+ ## none of an addon's or a suite's, then gdUnit4's suite. Only GDScript shares the engine's reading,
190
+ ## and a sandboxed mod's SafeGDScript class would load the sandbox's compiler, some 290 MB and up to
191
+ ## 2.5 s, for nothing.
192
+ static func held_classes(
193
+ classes: Array[Dictionary] = ProjectSettings.get_global_class_list()
194
+ ) -> PackedStringArray:
195
+ var held: PackedStringArray = []
196
+ for known: Dictionary in classes:
197
+ var path: String = known["path"]
198
+ var own: bool = not path.begins_with("res://addons/") and not path.ends_with("_test.gd")
199
+ if own and known["language"] == &"GDScript":
200
+ var _own: bool = held.append(str(known["class"]))
201
+ var _suite: bool = held.append("GdUnitTestSuite")
202
+ return held
203
+
204
+
205
+ ## What the file after [param flag] in [param args] names, one a line in order, or none when no file
206
+ ## is named: gdUnit4 then says it found nothing, which a gate refuses.
207
+ static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
208
+ var path: String = argument_after(args, flag)
209
+ if path.is_empty():
210
+ return PackedStringArray()
211
+ var named: PackedStringArray = []
212
+ for line: String in FileAccess.get_file_as_string(path).split("\n", false):
213
+ if not line.strip_edges().is_empty():
214
+ var _named: bool = named.append(line.strip_edges())
215
+ return named
216
+
217
+
218
+ ## Whether [param entry] of a [constant SKIPPING] file names one case of one suite file, as
219
+ ## `res://suite.gd:test_case`, rather than a suite or a case by gdUnit4's looser names, which go to
220
+ ## gdUnit4 as they are.
221
+ ## Whether [param entry] names one row of a parameterised case, as `res://suite.gd:test_case:row`.
222
+ static func is_one_row(entry: String) -> bool:
223
+ var at: int = entry.rfind(":")
224
+ var row: String = entry.substr(at + 1)
225
+ return (
226
+ at > 0
227
+ and row.is_valid_int()
228
+ and not row.begins_with("-")
229
+ and is_one_case(entry.substr(0, at))
230
+ )
231
+
232
+
233
+ static func is_one_case(entry: String) -> bool:
234
+ var at: int = entry.rfind(":")
235
+ return (
236
+ entry.begins_with("res://")
237
+ and at > "res://".length()
238
+ and entry.substr(0, at).ends_with(".gd")
239
+ and entry.substr(at + 1).is_valid_ascii_identifier()
240
+ )
241
+
242
+
243
+ ## The row shares [param lines] of a [constant ROWS] file give, by case.
244
+ static func row_shares_of(lines: PackedStringArray) -> Dictionary[String, GdhRowShare]:
245
+ var shares: Dictionary[String, GdhRowShare] = {}
246
+ for line: String in lines:
247
+ var parts: PackedStringArray = line.split("\t")
248
+ if parts.size() < 2:
249
+ continue
250
+ var share: GdhRowShare = GdhRowShare.new()
251
+ share.only = parts[1] == "only"
252
+ for row: String in parts.slice(2):
253
+ if row.is_valid_int():
254
+ var _added: bool = share.rows.append(row.to_int())
255
+ shares[parts[0]] = share
256
+ return shares
257
+
258
+
259
+ ## The argument after [param flag] in [param args], or "" when there is none.
260
+ static func argument_after(args: PackedStringArray, flag: String) -> String:
261
+ var at: int = args.find(flag)
262
+ if at < 0 or at + 1 >= args.size():
263
+ return ""
264
+ return args[at + 1]
265
+
266
+
267
+ ## What each of [param suites] declares in [param constant], by suite path and then case name, read
268
+ ## off the compiled suite's constants so a declaration is whatever the engine made of it. A
269
+ ## directory listed is walked for its scripts, as gdUnit4 walks it for suites. Only a script whose
270
+ ## source names the constant is loaded: gdUnit4 stops on a suite that does not load only when its
271
+ ## own load is the one that fails, so a broken suite loaded here first let the rest of its directory
272
+ ## run.
273
+ static func expected_by_suite(suites: PackedStringArray, constant: String) -> Dictionary:
274
+ var expected: Dictionary = {}
275
+ for path: String in scripts_in(suites):
276
+ if (
277
+ not FileAccess.file_exists(path)
278
+ or not FileAccess.get_file_as_string(path).contains(constant)
279
+ ):
280
+ continue
281
+ var suite: Script = load(path)
282
+ var declared: Variant = suite.get_script_constant_map().get(constant)
283
+ if declared is Dictionary:
284
+ expected[path] = declared
285
+ return expected
286
+
287
+
288
+ ## The scripts [param paths] names: each script, and every script under each directory.
289
+ static func scripts_in(paths: PackedStringArray) -> PackedStringArray:
290
+ var found: PackedStringArray = []
291
+ var pending: PackedStringArray = []
292
+ for path: String in paths:
293
+ if path.ends_with(".gd"):
294
+ var _script: bool = found.append(path)
295
+ else:
296
+ var _directory: bool = pending.append(path.trim_suffix("/"))
297
+ while not pending.is_empty():
298
+ var directory: String = pending[pending.size() - 1]
299
+ pending.remove_at(pending.size() - 1)
300
+ for file: String in DirAccess.get_files_at(directory):
301
+ if file.ends_with(".gd"):
302
+ var _file: bool = found.append("%s/%s" % [directory, file])
303
+ for inner: String in DirAccess.get_directories_at(directory):
304
+ var _inner: bool = pending.append("%s/%s" % [directory, inner])
305
+ return found
306
+
307
+
308
+ static func write_expected(path: String, suites: PackedStringArray) -> void:
309
+ var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
310
+ if file == null:
311
+ push_error(
312
+ "Could not write the expected errors to %s: %s" % [path, FileAccess.get_open_error()]
313
+ )
314
+ return
315
+ var expected: Dictionary = {}
316
+ for kind: String in EXPECTED:
317
+ expected[kind] = expected_by_suite(suites, EXPECTED[kind])
318
+ var _stored: bool = file.store_string(JSON.stringify(expected))
319
+ file.close()
320
+
321
+
79
322
  ## gdUnit4's runner reading its options after this script's path rather than after its own tool's,
80
323
  ## which it looks for by name and otherwise finds none of them, printing its help.
81
324
  class GdhRunner:
@@ -90,14 +333,16 @@ class GdhRunner:
90
333
  var together: GDScript = null
91
334
  ## The cases dealt by their rows, by `res://suite.gd:test_case`.
92
335
  var row_shares: Dictionary[String, GdhRowShare] = {}
93
- ## The cases to leave out, by `res://suite.gd:test_case` of the suite running them. gdUnit4's own
94
- ## list is matched against the script declaring a case, which for a case a suite inherits is its
95
- ## base, so an inherited case left out under its suite ran in every engine the suite was dealt to,
96
- ## and a case of `res://tests/a_test.gd` was also left out of `res://more/tests/a_test.gd`.
336
+ ## The cases to leave out, by `res://suite.gd:test_case` of the suite running them. gdUnit4's
337
+ ## own list is matched against the script declaring a case, which for a case a suite inherits is
338
+ ## its base, so an inherited case left out under its suite ran in every engine the suite was
339
+ ## dealt to, and a case of `res://tests/a_test.gd` was also left out of
340
+ ## `res://more/tests/a_test.gd`.
97
341
  var left_out: Dictionary[String, bool] = {}
98
342
  ## The rows of parameterised cases to leave out, by `res://suite.gd:test_case:row`.
99
343
  var left_out_rows: Dictionary[String, bool] = {}
100
- ## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups against.
344
+ ## The cases gdUnit4 said it finished, which [GdhSessionCheck] holds the session's lookups
345
+ ## against.
101
346
  var cases_ended: int = 0
102
347
  ## The marker of the shutdown walk's oldest end, made as the run starts.
103
348
  var walk_scripts: Array[GDScript] = []
@@ -135,8 +380,8 @@ class GdhRunner:
135
380
  return options_of(OS.get_cmdline_args(), own_path)
136
381
 
137
382
  ## gdUnit4's own discovery, with each case filed under the suite it was found in before it is
138
- ## asked whether to leave the case out. gdUnit4 files a case under the script declaring it, which
139
- ## for a case a suite inherits is its base: its reports added such a case to no suite, so
383
+ ## asked whether to leave the case out. gdUnit4 files a case under the script declaring it,
384
+ ## which for a case a suite inherits is its base: its reports added such a case to no suite, so
140
385
  ## project_test counted none of them, and a parameterised one ran as a suite of the base's own,
141
386
  ## outside the suite inheriting it and its hooks, and was judged a case that suite never ran.
142
387
  func discover_tests() -> Array[GdUnitTestCase]:
@@ -169,7 +414,9 @@ class GdhRunner:
169
414
  func is_skipped(test: GdUnitTestCase) -> bool:
170
415
  if left_out.has("%s:%s" % [test.suite_resource_path, test.test_name]) or super(test):
171
416
  return true
172
- var row: String = "%s:%s:%d" % [test.suite_resource_path, test.test_name, test.attribute_index]
417
+ var row: String = (
418
+ "%s:%s:%d" % [test.suite_resource_path, test.test_name, test.attribute_index]
419
+ )
173
420
  if test.attribute_index >= 0 and left_out_rows.has(row):
174
421
  print(
175
422
  (
@@ -178,19 +425,27 @@ class GdhRunner:
178
425
  )
179
426
  )
180
427
  return true
181
- var share: GdhRowShare = row_shares.get("%s:%s" % [test.suite_resource_path, test.test_name], null)
428
+ var share: GdhRowShare = row_shares.get(
429
+ "%s:%s" % [test.suite_resource_path, test.test_name], null
430
+ )
182
431
  if share == null or not share.leaves_out(test.attribute_index):
183
432
  return false
184
- print("%s %s > %s:%d" % [LEFT_OUT, test.suite_resource_path, test.test_name, test.attribute_index])
433
+ print(
434
+ (
435
+ "%s %s > %s:%d"
436
+ % [LEFT_OUT, test.suite_resource_path, test.test_name, test.attribute_index]
437
+ )
438
+ )
185
439
  return true
186
440
 
187
441
  ## The session gdUnit4 has just made, swapped for a [GdhIndexedSession] of the same cases as it
188
- ## is announced: everything that reads the session, the hooks, the cases and the console printing
189
- ## every case, is handed it after this.
442
+ ## is announced: everything that reads the session, the hooks, the cases and the console
443
+ ## printing every case, is handed it after this.
190
444
  func _on_gdunit_event(event: GdUnitEvent) -> void:
191
445
  match event.type():
192
446
  GdUnitEvent.SESSION_START:
193
- _test_session = GdhIndexedSession.of(_test_session)
447
+ if not GdhIndexedSession.indexes_itself(_test_session):
448
+ _test_session = GdhIndexedSession.of(_test_session)
194
449
  GdUnitEvent.TESTSUITE_BEFORE:
195
450
  suite_running = event.resource_path()
196
451
  orphans_at_suite = orphan_ids()
@@ -216,8 +471,12 @@ class GdhRunner:
216
471
  if not case_began:
217
472
  say_left("before or after", left)
218
473
  return
219
- say_left("before", left.filter(func(id: int) -> bool: return orphans_from_before.has(id)))
220
- say_left("after", left.filter(func(id: int) -> bool: return not orphans_from_before.has(id)))
474
+ say_left(
475
+ "before", left.filter(func(id: int) -> bool: return orphans_from_before.has(id))
476
+ )
477
+ say_left(
478
+ "after", left.filter(func(id: int) -> bool: return not orphans_from_before.has(id))
479
+ )
221
480
 
222
481
  ## The orphans now that were not there in [param before] and no line has named yet.
223
482
  func new_orphans(before: Dictionary[int, bool]) -> Array[int]:
@@ -237,7 +496,9 @@ class GdhRunner:
237
496
  orphans_named[id] = true
238
497
  if named.size() < ORPHANS_NAMED:
239
498
  var _named: bool = named.append(described(instance_from_id(id) as Node))
240
- var more: String = "" if ids.size() <= named.size() else ", and %d more" % (ids.size() - named.size())
499
+ var more: String = (
500
+ "" if ids.size() <= named.size() else ", and %d more" % (ids.size() - named.size())
501
+ )
241
502
  print(
242
503
  (
243
504
  "%s %s > %s left %d: %s%s"
@@ -268,8 +529,8 @@ class GdhRunner:
268
529
  said += " of " + script.resource_path
269
530
  return said
270
531
 
271
- ## [param args] with everything up to the script at [param own] given as gdUnit4's tool name. The
272
- ## path is compared with its separators and, on Windows, its case folded, since the engine
532
+ ## [param args] with everything up to the script at [param own] given as gdUnit4's tool name.
533
+ ## The path is compared with its separators and, on Windows, its case folded, since the engine
273
534
  ## reports the script as `X:/a/listed.gd` when it was started as `X:\a\listed.gd`.
274
535
  static func options_of(args: PackedStringArray, own: String) -> PackedStringArray:
275
536
  var wanted: String = spelt(own)
@@ -309,13 +570,14 @@ class GdhRowShare:
309
570
  class GdhIndexedSession:
310
571
  extends GdUnitTestSession
311
572
 
312
- ## The cases found by id, which [GdhSessionCheck] reads to tell that the console used this session.
573
+ ## The cases found by id, which [GdhSessionCheck] reads to tell that the console used this
574
+ ## session.
313
575
  var lookups: int = 0
314
576
  var _by_id: Dictionary[String, GdUnitTestCase] = {}
315
577
 
316
578
  func _init(cases: Array[GdUnitTestCase], reports_in: String) -> void:
317
579
  super(cases, reports_in)
318
- for test: GdUnitTestCase in _test_cases:
580
+ for test: GdUnitTestCase in cases:
319
581
  _by_id[str(test.guid)] = test
320
582
 
321
583
  func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
@@ -328,7 +590,20 @@ class GdhIndexedSession:
328
590
  static func of(made: GdUnitTestSession) -> GdUnitTestSession:
329
591
  if made is GdhIndexedSession:
330
592
  return made
331
- return GdhIndexedSession.new(made._test_cases, made.report_path)
593
+ # Read by name, since a gdUnit4 whose session indexes its cases has no such member and a
594
+ # script naming it would not compile there.
595
+ var cases: Array[GdUnitTestCase] = []
596
+ var held: Variant = made.get(&"_test_cases")
597
+ if held is Array:
598
+ var listed: Array = held
599
+ cases.assign(listed)
600
+ return GdhIndexedSession.new(cases, made.report_path)
601
+
602
+ ## Whether [param made] already finds a case by its id in a dictionary, as a gdUnit4 session
603
+ ## with [code]test_count()[/code] does: gdUnit4 added both in one change, and took away the
604
+ ## array of cases [method of] reads.
605
+ static func indexes_itself(made: GdUnitTestSession) -> bool:
606
+ return made.has_method(&"test_count")
332
607
 
333
608
 
334
609
  ## A session hook of the runner's own, failing the run when the [GdhIndexedSession] is not the
@@ -346,16 +621,20 @@ class GdhSessionCheck:
346
621
  _runner = runner
347
622
 
348
623
  func startup(session: GdUnitTestSession) -> GdUnitResult:
349
- if session is GdhIndexedSession:
624
+ if session is GdhIndexedSession or GdhIndexedSession.indexes_itself(session):
350
625
  return GdUnitResult.success()
351
- return GdUnitResult.error(
352
- (
626
+ return (
627
+ GdUnitResult
628
+ . error(
353
629
  (
354
- "gdharness test: gdUnit4 %s handed its session hooks the session before announcing it, so"
355
- + " the runner's indexed session is not the one they read. This gdUnit4 starts a session"
356
- + " differently from the ones gdharness knows; report it to gdharness with the version."
630
+ (
631
+ "gdharness test: gdUnit4 %s handed its session hooks the session before "
632
+ + "announcing it, so the runner's indexed session is not the one they "
633
+ + "read. This gdUnit4 starts a session differently from the ones gdharness "
634
+ + "knows; report it to gdharness with the version."
635
+ )
636
+ % GdUnit4Version.current()
357
637
  )
358
- % GdUnit4Version.current()
359
638
  )
360
639
  )
361
640
 
@@ -363,14 +642,18 @@ class GdhSessionCheck:
363
642
  var indexed: GdhIndexedSession = session as GdhIndexedSession
364
643
  if indexed == null or _runner.cases_ended == 0 or indexed.lookups > 0:
365
644
  return GdUnitResult.success()
366
- return GdUnitResult.error(
367
- (
645
+ return (
646
+ GdUnitResult
647
+ . error(
368
648
  (
369
- "gdharness test: gdUnit4 %s ran %d cases without looking one up in the runner's indexed"
370
- + " session, so its console reads another session. This gdUnit4 starts a session"
371
- + " differently from the ones gdharness knows; report it to gdharness with the version."
649
+ (
650
+ "gdharness test: gdUnit4 %s ran %d cases without looking one up in the "
651
+ + "runner's indexed session, so its console reads another session. This "
652
+ + "gdUnit4 starts a session differently from the ones gdharness knows; "
653
+ + "report it to gdharness with the version."
654
+ )
655
+ % [GdUnit4Version.current(), _runner.cases_ended]
372
656
  )
373
- % [GdUnit4Version.current(), _runner.cases_ended]
374
657
  )
375
658
  )
376
659
 
@@ -396,8 +679,8 @@ class GdhShutdownWalk:
396
679
  static func marker(said: String) -> Array[GDScript]:
397
680
  var printing: GDScript = GDScript.new()
398
681
  printing.source_code = (
399
- 'extends RefCounted\n\nvar said: String = ""\n\n\nfunc _notification(what: int) -> void:\n'
400
- + "\tif what == NOTIFICATION_PREDELETE:\n\t\tprint(said)\n"
682
+ 'extends RefCounted\n\nvar said: String = ""\n\n\nfunc _notification(what: int) -> '
683
+ + "void:\n\tif what == NOTIFICATION_PREDELETE:\n\t\tprint(said)\n"
401
684
  )
402
685
  var marking: GDScript = GDScript.new()
403
686
  marking.source_code = (
@@ -434,233 +717,3 @@ class GdhShutdownWalk:
434
717
  if not inner.begins_with("."):
435
718
  var _inner: bool = pending.append(directory.path_join(inner))
436
719
  ThemeDB.set_meta(WALK_HOLD, held)
437
-
438
-
439
- func _initialize() -> void:
440
- _runner = started(OS.get_cmdline_user_args())
441
- _runner.own_path = get_script().resource_path
442
- root.add_child(_runner)
443
-
444
-
445
- ## As gdUnit4's own `GdUnitCmdTool.gd` ends, which names in a verbose run the nodes left orphaned.
446
- func _finalize() -> void:
447
- queue_delete(_runner)
448
- if OS.is_stdout_verbose():
449
- print("Orphan nodes left as the run ends:")
450
- Window.print_orphan_nodes()
451
-
452
-
453
- ## The runner over the suites [param given] lists, to be added to the tree, which starts it. Shared
454
- ## by this script and the suites scene of the tests addon, which loads this file from where the
455
- ## server ships it and calls this, since the addon itself names no gdUnit4 class.
456
- static func started(given: PackedStringArray) -> GdhRunner:
457
- # First, so the marker is older than every script the run loads.
458
- var oldest: Array[GDScript] = []
459
- if GdhShutdownWalk.guarded():
460
- oldest = GdhShutdownWalk.marker(WALK_ENDED)
461
- OS.low_processor_usage_mode_sleep_usec = FRAME_SLEEP_USEC
462
- var at: int = given.find(GDUNIT)
463
- var asked: PackedStringArray = given if at < 0 else given.slice(0, at)
464
- var unread: PackedStringArray = []
465
- for flag: String in REPORTS_OFF:
466
- if asked.has(flag):
467
- var _unread: bool = unread.append(REPORTS_OFF[flag])
468
- var hooks: GdUnitTestSessionHookService = GdUnitTestSessionHookService.instance()
469
- var off: PackedStringArray = unread_off(hooks, unread)
470
- for path: String in unread:
471
- if not off.has(path):
472
- push_error(
473
- (
474
- (
475
- "gdharness test: gdUnit4 %s registers no session hook %s, so the report it writes is not"
476
- + " turned off; report it to gdharness with the version."
477
- )
478
- % [GdUnit4Version.current(), path]
479
- )
480
- )
481
- var suites: PackedStringArray = listed(asked, LISTED)
482
- var runner: GdhRunner = GdhRunner.new()
483
- runner.walk_scripts = oldest
484
- if at >= 0:
485
- runner.given = given.slice(at + 1)
486
- if asked.has(TOGETHER):
487
- runner.together = compiled_together(suites)
488
- var expected_out: String = argument_after(asked, EXPECTED_OUT)
489
- if not expected_out.is_empty():
490
- write_expected(expected_out, suites)
491
- runner.add_test_suites(suites)
492
- for entry: String in listed(asked, SKIPPING):
493
- if is_one_row(entry):
494
- runner.left_out_rows[entry] = true
495
- elif is_one_case(entry):
496
- runner.left_out[entry] = true
497
- else:
498
- runner.skip_test_suite(entry)
499
- runner.row_shares = row_shares_of(listed(asked, ROWS))
500
- # Added to the list itself rather than through `register`, which saves every hook it holds into
501
- # `project.godot`.
502
- hooks.enigne_hooks.append(GdhSessionCheck.new(runner))
503
- return runner
504
-
505
-
506
- ## Turns off the hooks among [param hooks] whose scripts [param unread] names, answering the scripts
507
- ## of those it turned off. Through the mark gdUnit4's own `is_enabled` reads, rather than its
508
- ## `enable_hook`, which writes the setting into `project.godot`: a run changes nothing in the tree.
509
- static func unread_off(hooks: GdUnitTestSessionHookService, unread: PackedStringArray) -> PackedStringArray:
510
- var off: PackedStringArray = []
511
- for hook: GdUnitTestSessionHook in hooks.enigne_hooks:
512
- var path: String = hook.get_script().resource_path
513
- if unread.has(path):
514
- hook.set_meta("enabled", false)
515
- var _added: bool = off.append(path)
516
- return off
517
-
518
-
519
- ## [param suites] compiled inside one script, answered for the caller to hold so gdUnit4's own loads
520
- ## find them compiled. The engine keeps what it read of a class only while the compile that asked for
521
- ## it runs (`GDScriptCache`), so a suite compiled on its own reads again every class it reaches a
522
- ## member of, some seventy milliseconds a suite. As the preloads of one script naming every class of
523
- ## the game's own first, held from that script's compile, they share one reading of each: two hundred
524
- ## suites compiled in a third of the time they took one at a time. The names are public, since a
525
- ## private one unread is a warning and every warning an error, and each a variable, since a class is
526
- ## no constant expression. A suite that does not compile fails here and again when gdUnit4 loads it,
527
- ## which reports it as it would have. A directory listed is left to gdUnit4 to walk.
528
- static func compiled_together(suites: PackedStringArray) -> GDScript:
529
- var lines: PackedStringArray = ["extends RefCounted"]
530
- var at: int = 0
531
- for named: String in held_classes():
532
- var _held: bool = lines.append("var held_%d: Variant = %s" % [at, named])
533
- at += 1
534
- at = 0
535
- for path: String in suites:
536
- if not path.ends_with(".gd"):
537
- continue
538
- var _suite: bool = lines.append('var suite_%d: Variant = preload("%s")' % [at, path])
539
- at += 1
540
- var together: GDScript = GDScript.new()
541
- together.source_code = "\n".join(lines) + "\n"
542
- var _compiled: Error = together.reload()
543
- return together
544
-
545
-
546
- ## The classes [method compiled_together] names before the suites: the game's own GDScript classes,
547
- ## none of an addon's or a suite's, then gdUnit4's suite. Only GDScript shares the engine's reading,
548
- ## and a sandboxed mod's SafeGDScript class would load the sandbox's compiler, some 290 MB and up to
549
- ## 2.5 s, for nothing.
550
- static func held_classes(
551
- classes: Array[Dictionary] = ProjectSettings.get_global_class_list()
552
- ) -> PackedStringArray:
553
- var held: PackedStringArray = []
554
- for known: Dictionary in classes:
555
- var path: String = known["path"]
556
- var own: bool = not path.begins_with("res://addons/") and not path.ends_with("_test.gd")
557
- if own and known["language"] == &"GDScript":
558
- var _own: bool = held.append(str(known["class"]))
559
- var _suite: bool = held.append("GdUnitTestSuite")
560
- return held
561
-
562
-
563
- ## What the file after [param flag] in [param args] names, one a line in order, or none when no file
564
- ## is named: gdUnit4 then says it found nothing, which a gate refuses.
565
- static func listed(args: PackedStringArray, flag: String) -> PackedStringArray:
566
- var path: String = argument_after(args, flag)
567
- if path.is_empty():
568
- return PackedStringArray()
569
- var named: PackedStringArray = []
570
- for line: String in FileAccess.get_file_as_string(path).split("\n", false):
571
- if not line.strip_edges().is_empty():
572
- var _named: bool = named.append(line.strip_edges())
573
- return named
574
-
575
-
576
- ## Whether [param entry] of a [constant SKIPPING] file names one case of one suite file, as
577
- ## `res://suite.gd:test_case`, rather than a suite or a case by gdUnit4's looser names, which go to
578
- ## gdUnit4 as they are.
579
- ## Whether [param entry] names one row of a parameterised case, as `res://suite.gd:test_case:row`.
580
- static func is_one_row(entry: String) -> bool:
581
- var at: int = entry.rfind(":")
582
- var row: String = entry.substr(at + 1)
583
- return at > 0 and row.is_valid_int() and not row.begins_with("-") and is_one_case(entry.substr(0, at))
584
-
585
-
586
- static func is_one_case(entry: String) -> bool:
587
- var at: int = entry.rfind(":")
588
- return (
589
- entry.begins_with("res://")
590
- and at > "res://".length()
591
- and entry.substr(0, at).ends_with(".gd")
592
- and entry.substr(at + 1).is_valid_ascii_identifier()
593
- )
594
-
595
-
596
- ## The row shares [param lines] of a [constant ROWS] file give, by case.
597
- static func row_shares_of(lines: PackedStringArray) -> Dictionary[String, GdhRowShare]:
598
- var shares: Dictionary[String, GdhRowShare] = {}
599
- for line: String in lines:
600
- var parts: PackedStringArray = line.split("\t")
601
- if parts.size() < 2:
602
- continue
603
- var share: GdhRowShare = GdhRowShare.new()
604
- share.only = parts[1] == "only"
605
- for row: String in parts.slice(2):
606
- if row.is_valid_int():
607
- var _added: bool = share.rows.append(row.to_int())
608
- shares[parts[0]] = share
609
- return shares
610
-
611
-
612
- ## The argument after [param flag] in [param args], or "" when there is none.
613
- static func argument_after(args: PackedStringArray, flag: String) -> String:
614
- var at: int = args.find(flag)
615
- if at < 0 or at + 1 >= args.size():
616
- return ""
617
- return args[at + 1]
618
-
619
-
620
- ## What each of [param suites] declares in [param constant], by suite path and then case name, read
621
- ## off the compiled suite's constants so a declaration is whatever the engine made of it. A directory
622
- ## listed is walked for its scripts, as gdUnit4 walks it for suites. Only a script whose source names
623
- ## the constant is loaded: gdUnit4 stops on a suite that does not load only when its own load is the
624
- ## one that fails, so a broken suite loaded here first let the rest of its directory run.
625
- static func expected_by_suite(suites: PackedStringArray, constant: String) -> Dictionary:
626
- var expected: Dictionary = {}
627
- for path: String in scripts_in(suites):
628
- if not FileAccess.file_exists(path) or not FileAccess.get_file_as_string(path).contains(constant):
629
- continue
630
- var suite: Script = load(path)
631
- var declared: Variant = suite.get_script_constant_map().get(constant)
632
- if declared is Dictionary:
633
- expected[path] = declared
634
- return expected
635
-
636
-
637
- ## The scripts [param paths] names: each script, and every script under each directory.
638
- static func scripts_in(paths: PackedStringArray) -> PackedStringArray:
639
- var found: PackedStringArray = []
640
- var pending: PackedStringArray = []
641
- for path: String in paths:
642
- if path.ends_with(".gd"):
643
- var _script: bool = found.append(path)
644
- else:
645
- var _directory: bool = pending.append(path.trim_suffix("/"))
646
- while not pending.is_empty():
647
- var directory: String = pending[pending.size() - 1]
648
- pending.remove_at(pending.size() - 1)
649
- for file: String in DirAccess.get_files_at(directory):
650
- if file.ends_with(".gd"):
651
- var _file: bool = found.append("%s/%s" % [directory, file])
652
- for inner: String in DirAccess.get_directories_at(directory):
653
- var _inner: bool = pending.append("%s/%s" % [directory, inner])
654
- return found
655
-
656
-
657
- static func write_expected(path: String, suites: PackedStringArray) -> void:
658
- var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
659
- if file == null:
660
- push_error("Could not write the expected errors to %s: %s" % [path, FileAccess.get_open_error()])
661
- return
662
- var expected: Dictionary = {}
663
- for kind: String in EXPECTED:
664
- expected[kind] = expected_by_suite(suites, EXPECTED[kind])
665
- var _stored: bool = file.store_string(JSON.stringify(expected))
666
- file.close()