summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd
diff options
context:
space:
mode:
authorradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
committerradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
commita89e744ed0e8c759caf041a580112365bbfe4303 (patch)
treedd61e320904176995727e86a857d00c90d4933ba /addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd
parent6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff)
escoria updateHEADmain
Diffstat (limited to 'addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd')
-rw-r--r--addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd154
1 files changed, 154 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd
new file mode 100644
index 0000000..5d8b631
--- /dev/null
+++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd
@@ -0,0 +1,154 @@
+class_name GdUnitSpyBuilder
+extends GdUnitClassDoubler
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd")
+const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"]
+
+
+static func build(to_spy: Variant, debug_write := false) -> Variant:
+ if GdObjects.is_singleton(to_spy):
+ @warning_ignore("unsafe_cast")
+ push_error("Spy on a Singleton is not allowed! '%s'" % (to_spy as Object).get_class())
+ return null
+
+ # if resource path load it before
+ if GdObjects.is_scene_resource_path(to_spy):
+ var scene_resource_path :String = to_spy
+ if not FileAccess.file_exists(scene_resource_path):
+ push_error("Can't build spy on scene '%s'! The given resource not exists!" % scene_resource_path)
+ return null
+ var scene_to_spy: PackedScene = load(scene_resource_path)
+ return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write)
+ # spy checked PackedScene
+ if GdObjects.is_scene(to_spy):
+ var scene_to_spy: PackedScene = to_spy
+ return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write)
+ # spy checked a scene instance
+ if GdObjects.is_instance_scene(to_spy):
+ @warning_ignore("unsafe_cast")
+ return spy_on_scene(to_spy as Node, debug_write)
+
+ var excluded_functions := []
+ if to_spy is Callable:
+ @warning_ignore("unsafe_cast")
+ to_spy = CallableDoubler.new(to_spy as Callable)
+ excluded_functions = CallableDoubler.excluded_functions()
+
+ var spy := spy_on_script(to_spy, excluded_functions, debug_write)
+ if spy == null:
+ return null
+ var spy_instance: Object = spy.new()
+ @warning_ignore("unsafe_method_access")
+ # we do not call the original implementation for _ready and all input function, this is actualy done by the engine
+ spy_instance.__init(["_input", "_gui_input", "_input_event", "_unhandled_input"])
+ @warning_ignore("unsafe_cast")
+ copy_properties(to_spy as Object, spy_instance)
+ @warning_ignore("return_value_discarded")
+ GdUnitObjectInteractions.reset(spy_instance)
+ return register_auto_free(spy_instance)
+
+
+static func get_class_info(clazz :Variant) -> Dictionary:
+ var clazz_path := GdObjects.extract_class_path(clazz)
+ var clazz_name :String = GdObjects.extract_class_name(clazz).value()
+ return {
+ "class_name" : clazz_name,
+ "class_path" : clazz_path
+ }
+
+
+static func spy_on_script(instance: Variant, function_excludes: PackedStringArray, debug_write: bool) -> GDScript:
+ if GdArrayTools.is_array_type(instance):
+ if GdUnitSettings.is_verbose_assert_errors():
+ push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % type_string(typeof(instance)))
+ return null
+ var class_info := get_class_info(instance)
+ var clazz_name :String = class_info.get("class_name")
+ var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name])
+ if not GdObjects.is_instance(instance):
+ if GdUnitSettings.is_verbose_assert_errors():
+ push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy(<instance>)'" % [clazz_name])
+ return null
+
+ @warning_ignore("unsafe_method_access")
+ var spy_template := SPY_TEMPLATE.source_code.format({
+ "instance_id" : abs(instance.get_instance_id()),
+ "gdunit_source_class": clazz_name if clazz_path.is_empty() else clazz_path[0]
+ })
+ @warning_ignore("unsafe_cast")
+ var lines := load_template(spy_template, class_info)
+ @warning_ignore("unsafe_cast")
+ lines += double_functions(instance as Object, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes)
+ # We disable warning/errors for inferred_declaration
+ if Engine.get_version_info().hex >= 0x40400:
+ lines.insert(0, '@warning_ignore_start("inferred_declaration")')
+ lines.append('@warning_ignore_restore("inferred_declaration")')
+
+ var spy := GDScript.new()
+ spy.source_code = "\n".join(lines)
+ spy.resource_name = "Spy%s.gd" % clazz_name
+ spy.resource_path = GdUnitFileAccess.create_temp_dir("spy") + "/Spy%s_%d.gd" % [clazz_name, Time.get_ticks_msec()]
+
+ if debug_write:
+ @warning_ignore("return_value_discarded")
+ DirAccess.remove_absolute(spy.resource_path)
+ @warning_ignore("return_value_discarded")
+ ResourceSaver.save(spy, spy.resource_path)
+ var error := spy.reload(true)
+ if error != OK:
+ push_error("Unexpected Error!, SpyBuilder error, please contact the developer.")
+ return null
+ return spy
+
+
+static func spy_on_scene(scene :Node, debug_write :bool) -> Object:
+ if scene.get_script() == null:
+ if GdUnitSettings.is_verbose_assert_errors():
+ push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path())
+ return null
+ # buils spy checked original script
+ @warning_ignore("unsafe_cast")
+ var scene_script :Object = (scene.get_script() as GDScript).new()
+ var spy := spy_on_script(scene_script, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write)
+ scene_script.free()
+ if spy == null:
+ return null
+
+ # we need to restore the original script properties to apply after script exchange
+ var original_properties := {}
+ for p in scene.get_property_list():
+ var property_name: String = p["name"]
+ var usage: int = p["usage"]
+ if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE) == PROPERTY_USAGE_SCRIPT_VARIABLE:
+ original_properties[property_name] = scene.get(property_name)
+
+ # exchage with spy
+ scene.set_script(spy)
+ # apply original script properties to the spy
+ for property_name: String in original_properties.keys():
+ scene.set(property_name, original_properties[property_name])
+
+ @warning_ignore("unsafe_method_access")
+ scene.__init()
+ return register_auto_free(scene)
+
+
+static func copy_properties(source :Object, dest :Object) -> void:
+ for property in source.get_property_list():
+ var property_name :String = property["name"]
+ var property_value :Variant = source.get(property_name)
+ if EXCLUDE_PROPERTIES_TO_COPY.has(property_name):
+ continue
+ #if dest.get(property_name) == null:
+ # prints("|%s|" % property_name, source.get(property_name))
+
+ # check for invalid name property
+ if property_name == "name" and property_value == "":
+ dest.set(property_name, "<empty>");
+ continue
+ dest.set(property_name, property_value)
+
+
+static func register_auto_free(obj :Variant) -> Variant:
+ return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj)