gdharness 1.2.35 → 1.2.37

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 (28) hide show
  1. package/build/cli.js +478 -100
  2. package/build/godot/addons/gdharness_editor/headless_check.gd +374 -0
  3. package/build/godot/addons/gdharness_editor/headless_check.gd.uid +1 -0
  4. package/build/godot/addons/gdharness_editor/property_values.gd +3 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd +345 -10
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +103 -5
  7. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -18
  8. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +24 -13
  9. package/build/godot/addons/gdharness_runtime/call_errors.gd +7 -2
  10. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +9 -5
  11. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +33 -0
  12. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +21 -4
  13. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +7 -5
  14. package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +116 -3
  16. package/build/godot/addons/gdharness_runtime/serialisation.gd +103 -5
  17. package/build/godot/operations/classdb_queries.gd +22 -0
  18. package/build/godot/operations/dependencies.gd +90 -20
  19. package/build/godot/operations/editor_tools.gd +57 -0
  20. package/build/godot/operations/gdscript_analysis.gd +45 -2
  21. package/build/godot/operations/gdscript_authoring.gd +5 -2
  22. package/build/godot/operations/godot_operations.gd +53 -2
  23. package/build/godot/operations/property_values.gd +3 -0
  24. package/build/godot/operations/rename_checks.gd +0 -19
  25. package/build/godot/operations/script_check.gd +98 -0
  26. package/build/godot/operations/serialisation.gd +103 -5
  27. package/build/index.js +466 -90
  28. package/package.json +1 -1
@@ -1,6 +1,5 @@
1
1
  extends RefCounted
2
2
 
3
- const GdscriptAuthoring = preload("gdscript_authoring.gd")
4
3
  const Log = preload("logger.gd")
5
4
 
6
5
  var _log: Log
@@ -63,21 +62,3 @@ static func _named(entries: Array[Dictionary], name: String) -> bool:
63
62
  if str(entry.get("name", "")) == name:
64
63
  return true
65
64
  return false
66
-
67
-
68
- # Whether each script compiles in a fresh engine, which reads the class cache as it starts and so
69
- # resolves every class the cache lists. The engine's reasons go to stderr and come back with the
70
- # answer. It names the scripts it checked rather than counting them, since a caller told one was
71
- # checked could not say which.
72
- func check_scripts(params: Dictionary) -> Dictionary:
73
- var paths: Array = params.get("paths", [])
74
- var checked: Array[String] = []
75
- var failed: Array[String] = []
76
- for value: Variant in paths:
77
- var path: String = str(value)
78
- checked.append(path)
79
- var script: Script = ResourceLoader.load(path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
80
- if not GdscriptAuthoring._parses(script):
81
- _log.error("Does not compile: " + path)
82
- failed.append(path)
83
- return {"checked": checked, "failed": failed}
@@ -0,0 +1,98 @@
1
+ extends RefCounted
2
+
3
+ ## Compiles scripts the way the engine does when it loads them, and answers what it raised.
4
+ ##
5
+ ## The engine reports a script that does not compile as errors raised from GDScript::reload, each
6
+ ## naming the file and line, and they reach a Logger as well as stderr. Measured on 4.7.2: a script
7
+ ## whose dependency does not compile reports the dependency's errors at the dependency's path and one
8
+ ## of its own at line 0, "Compile Error: Failed to compile depended scripts", and is still handed
9
+ ## back as a script that can_instantiate() calls instantiable. Warnings are not among them: the engine
10
+ ## sends those only to an editor or a debugger.
11
+ ##
12
+ ## A script that failed stays failed in the engine's script cache, which no ResourceLoader cache mode
13
+ ## reaches, and every script loaded after it that depends on it compiles in silence, reload() and all.
14
+ ## So a run ends at the first script that raised anything, and the rest go to a fresh engine.
15
+
16
+ const Read = preload("reading.gd")
17
+ const Log = preload("logger.gd")
18
+
19
+ ## Where the engine raises a script's compile errors from.
20
+ const RELOAD: String = "GDScript::reload"
21
+
22
+ var _log: Log
23
+
24
+
25
+ ## Every compile error the engine raises while it is attached, in order.
26
+ class Caught:
27
+ extends Logger
28
+
29
+ var raised: Array[Dictionary] = []
30
+
31
+ func _log_message(_message: String, _error: bool) -> void:
32
+ pass
33
+
34
+ # The engine's own signature for a Logger, eight parameters and not this project's to shape.
35
+ # gdlint:ignore = function-arguments-number
36
+ func _log_error(
37
+ function: String,
38
+ file: String,
39
+ line: int,
40
+ code: String,
41
+ rationale: String,
42
+ _editor_notify: bool,
43
+ _error_type: int,
44
+ _script_backtraces: Array[ScriptBacktrace],
45
+ ) -> void:
46
+ if function != RELOAD:
47
+ return
48
+ raised.append(
49
+ {"path": file, "line": line, "message": rationale if not rationale.is_empty() else code}
50
+ )
51
+
52
+
53
+ func _init(p_log: Log) -> void:
54
+ _log = p_log
55
+
56
+
57
+ ## Whether the scripts in [code]paths[/code] compile in a fresh engine, which reads the class cache
58
+ ## as it starts and so resolves every class the cache lists, up to and including the first that
59
+ ## raised an error. [code]checked[/code] names the scripts read, in order, and [code]failed[/code]
60
+ ## those that did not compile; [code]scripts[/code] holds each failed one with the errors raised in
61
+ ## its own file, and the other scripts that failed to compile while it was loaded. Read past the
62
+ ## resource cache, so a script loaded for an earlier one is compiled again for its own entry.
63
+ func check_scripts(params: Dictionary) -> Dictionary:
64
+ var asked: Array = params.get("paths", [])
65
+ var caught: Caught = Caught.new()
66
+ OS.add_logger(caught)
67
+ var checked: Array[String] = []
68
+ var failed: Array[String] = []
69
+ var scripts: Array[Dictionary] = []
70
+ for entry: Variant in asked:
71
+ var path: String = str(entry)
72
+ checked.append(path)
73
+ caught.raised.clear()
74
+ var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
75
+ var errors: Array[Dictionary] = []
76
+ var others: Array[String] = []
77
+ for error: Dictionary in caught.raised:
78
+ var at: String = str(error["path"])
79
+ if at != path:
80
+ if not at in others:
81
+ others.append(at)
82
+ continue
83
+ var found: Dictionary = {"line": Read.as_int(error["line"]), "message": str(error["message"])}
84
+ if not found in errors:
85
+ errors.append(found)
86
+ if loaded == null and errors.is_empty():
87
+ errors.append({"line": 0, "message": "The engine could not load it as a script."})
88
+ if errors.is_empty() and others.is_empty():
89
+ continue
90
+ if not errors.is_empty():
91
+ failed.append(path)
92
+ var one: Dictionary = {"path": path, "errors": errors}
93
+ if not others.is_empty():
94
+ one["dependencies_with_errors"] = others
95
+ scripts.append(one)
96
+ break
97
+ OS.remove_logger(caught)
98
+ return {"checked": checked, "failed": failed, "scripts": scripts}
@@ -16,6 +16,10 @@ const Read = preload("reading.gd")
16
16
  const NUMBERS: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]
17
17
  const TEXTS: Array[int] = [TYPE_STRING, TYPE_STRING_NAME, TYPE_NODE_PATH]
18
18
 
19
+ # 2^53. A JSON number is a double, and every whole number below this one in size has a double of
20
+ # its own; from here on two or more share each double, so one arriving tells nobody which was sent.
21
+ const EXACT: int = 9007199254740992
22
+
19
23
  # The types that cross as a plain JSON list and have to be built back into themselves. A list is
20
24
  # what a caller can write by hand and what the serialiser answers with, and the parameter or
21
25
  # property being written is the only thing that knows which of these it wanted.
@@ -53,6 +57,8 @@ const LIST_TYPES: Array[int] = [
53
57
  # type it wanted.
54
58
  const SERIALISERS: Dictionary = {
55
59
  TYPE_NIL: "_serialize_nil",
60
+ TYPE_INT: "_serialize_int",
61
+ TYPE_PACKED_INT64_ARRAY: "_serialize_ints",
56
62
  TYPE_FLOAT: "_serialize_float",
57
63
  TYPE_PACKED_FLOAT32_ARRAY: "_serialize_floats",
58
64
  TYPE_PACKED_FLOAT64_ARRAY: "_serialize_floats",
@@ -109,6 +115,8 @@ const BUILDERS: Dictionary = {
109
115
  "NodePath": "_build_node_path",
110
116
  "PackedByteArray": "_build_bytes",
111
117
  "float": "_build_float",
118
+ "int": "_build_int",
119
+ "RID": "_build_rid",
112
120
  "Dictionary": "_build_dictionary",
113
121
  }
114
122
 
@@ -136,6 +144,8 @@ const REQUIRED: Dictionary = {
136
144
  "NodePath": [["path"]],
137
145
  "PackedByteArray": [["base64"]],
138
146
  "float": [["value"]],
147
+ "int": [["value"]],
148
+ "RID": [["id"]],
139
149
  "Dictionary": [["entries"]],
140
150
  }
141
151
 
@@ -243,9 +253,40 @@ func deserialize_value(value: Variant) -> Variant:
243
253
  return fields
244
254
  if tag == "Dictionary" and not pairs(fields["entries"]):
245
255
  return fields
256
+ # Digits only: the tag is there to carry what a number cannot, and {"value": 5} meant for some
257
+ # other shape is not to be read as 5.
258
+ if tag == "int" and not (fields["value"] is String and _exact_int(fields["value"]) != null):
259
+ return fields
260
+ if tag == "RID" and _exact_int(fields["id"]) == null:
261
+ return fields
246
262
  return call(builder, fields)
247
263
 
248
264
 
265
+ ## [param held] as the whole number it spells exactly, or null: an int, a JSON number that is whole
266
+ ## and no larger than a double holds exactly, the digits of one, or the int tag carrying them. A
267
+ ## fraction, or a number past 2^53 that arrived as a number and so may already be a neighbour of the
268
+ ## one meant, is no id.
269
+ func _exact_int(held: Variant) -> Variant:
270
+ if held is int:
271
+ return held
272
+ if held is float:
273
+ var number: float = held
274
+ if is_finite(number) and number == floorf(number) and absf(number) < EXACT:
275
+ return int(number)
276
+ return null
277
+ if held is String:
278
+ var digits: String = held
279
+ var parsed: int = digits.to_int()
280
+ if digits.is_valid_int() and str(parsed) == digits:
281
+ return parsed
282
+ return null
283
+ if held is Dictionary:
284
+ var tagged: Dictionary = held
285
+ if str(tagged.get("_type", "")) == "int" and tagged.has("value"):
286
+ return _exact_int(tagged["value"])
287
+ return null
288
+
289
+
249
290
  ## The components a path reads off [param value], by the names GDScript gives them, or none for a
250
291
  ## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
251
292
  ## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
@@ -393,6 +434,11 @@ func fitted(value: Variant, type: int) -> Variant:
393
434
  if not simple.has(type) or not simple.has(typeof(rebuilt)):
394
435
  return rebuilt
395
436
 
437
+ # Digits are read as digits, since read as JSON they become a double and one past 2^53 a
438
+ # neighbour of the number written.
439
+ if rebuilt is String and type == TYPE_INT and _exact_int(rebuilt) != null:
440
+ return _exact_int(rebuilt)
441
+
396
442
  if rebuilt is String and type != TYPE_STRING:
397
443
  var text: String = rebuilt
398
444
  var parsed: Variant = Read.json_or_null(text)
@@ -408,13 +454,33 @@ func fitted(value: Variant, type: int) -> Variant:
408
454
  return type_convert(rebuilt, type)
409
455
 
410
456
 
457
+ ## Why [param given], meant for an int, may not be the number that was sent, or "" when it is. A
458
+ ## JSON number is a double wherever it is read, so a whole number from 2^53 on arrives as the nearest
459
+ ## double to the one written: 9007199254740993 sent as a number arrives as 9007199254740992, and a
460
+ ## seed written that way would be stored as its neighbour with the answer saying it was written.
461
+ static func inexact(given: Variant, type: int) -> String:
462
+ if type != TYPE_INT or (typeof(given) != TYPE_FLOAT and typeof(given) != TYPE_INT):
463
+ return ""
464
+ var number: float = Read.as_float(given)
465
+ if not is_finite(number) or absf(number) < EXACT:
466
+ return ""
467
+ return (
468
+ (
469
+ "%s is 2^53 or more, where a JSON number holds a whole number only to the nearest double,"
470
+ + ' so it may not be the number sent: send the digits as {"_type": "int", "value": "..."}'
471
+ + ", the form a read answers with"
472
+ )
473
+ % String.num(number, 0)
474
+ )
475
+
476
+
411
477
  ## How a value of [param type] is written to be read as one, for a refusal to name, or empty for a
412
478
  ## type that is not built from fields. A refusal naming only the two types left a caller to find
413
479
  ## the shape in the description of another op.
414
480
  static func written_as(type: int) -> String:
415
481
  var tag: String = type_string(type)
416
482
  var alternatives: Array = REQUIRED.get(tag, [])
417
- if alternatives.is_empty() or tag == "float" or tag == "Dictionary":
483
+ if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
418
484
  return ""
419
485
  var keys: Array = alternatives[0]
420
486
  var parts: Array[String] = []
@@ -530,10 +596,32 @@ func _serialize_node_path(value: NodePath) -> Dictionary:
530
596
  return {"path": str(value), "_type": "NodePath"}
531
597
 
532
598
 
533
- # An id rather than nothing, because two properties holding the same RID are worth telling apart
534
- # even though neither can be handed back to the engine.
599
+ # By its id, which is all an RID is from outside the server holding it, and which builds it back:
600
+ # a viewport's RID read off one call is what RenderingServer's render timing takes in the next.
535
601
  func _serialize_rid(value: RID) -> Dictionary:
536
- return {"id": value.get_id(), "_type": "RID"}
602
+ return {"id": _serialize_int(value.get_id()), "_type": "RID"}
603
+
604
+
605
+ # A whole number from 2^53 on as its digits, because JSON numbers are doubles on the server's side
606
+ # and on the engine's, and from there on neighbouring whole numbers share a double. A RefCounted's
607
+ # instance id is one, with its top bit set, and a resource uid another; read as numbers they came
608
+ # back as a neighbouring id, which named nothing or something else.
609
+ func _serialize_int(value: int) -> Variant:
610
+ if value > -EXACT and value < EXACT:
611
+ return value
612
+ return {"_type": "int", "value": str(value)}
613
+
614
+
615
+ # A packed list of 64-bit numbers stays the JSON list it is unless something in it is too large for
616
+ # a double to hold.
617
+ func _serialize_ints(value: PackedInt64Array) -> Variant:
618
+ for number: int in value:
619
+ if number <= -EXACT or number >= EXACT:
620
+ var items: Array = []
621
+ for each: int in value:
622
+ items.append(_serialize_int(each))
623
+ return items
624
+ return value
537
625
 
538
626
 
539
627
  # A bound method, said in the terms a caller could act on: what it is on, and what it is called.
@@ -672,7 +760,7 @@ func _describe_object(value: Object) -> Dictionary:
672
760
  # in a costume and reads as if the object had been asked and had nothing to say.
673
761
  if value.has_method("_to_string"):
674
762
  described["says"] = value.to_string()
675
- described["id"] = value.get_instance_id()
763
+ described["id"] = _serialize_int(value.get_instance_id())
676
764
  return described
677
765
 
678
766
 
@@ -808,6 +896,16 @@ func _build_float(fields: Dictionary) -> float:
808
896
  return Read.as_float(fields)
809
897
 
810
898
 
899
+ func _build_int(fields: Dictionary) -> int:
900
+ var exact: int = _exact_int(fields["value"])
901
+ return exact
902
+
903
+
904
+ func _build_rid(fields: Dictionary) -> RID:
905
+ var id: int = _exact_int(fields["id"])
906
+ return rid_from_int64(id)
907
+
908
+
811
909
  ## The entries of a dictionary whose keys are not all text. JSON carries every number as a float,
812
910
  ## and a dictionary keys 1.0 apart from 1, so a whole-number key is built as an int: the key a
813
911
  ## game uses far more often, and one JSON cannot tell from the float.