summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/doubler
diff options
context:
space:
mode:
Diffstat (limited to 'addons/gdUnit4/src/doubler')
-rw-r--r--addons/gdUnit4/src/doubler/CallableDoubler.gd156
-rw-r--r--addons/gdUnit4/src/doubler/CallableDoubler.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdFunctionDoubler.gd4
-rw-r--r--addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd119
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd336
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd10
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd53
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd84
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid1
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd8
-rw-r--r--addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid1
16 files changed, 778 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd b/addons/gdUnit4/src/doubler/CallableDoubler.gd
new file mode 100644
index 0000000..1bc78a1
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd
@@ -0,0 +1,156 @@
+## The helper class to allow to double Callable
+## Is just a wrapper to the original callable with the same function signature.
+##
+## Due to interface conflicts between 'Callable' and 'Object',
+## it is not possible to stub the 'call' and 'call_deferred' methods.
+##
+## The Callable interface and the Object class have overlapping method signatures,
+## which causes conflicts when attempting to stub these methods.
+## As a result, you cannot create stubs for 'call' and 'call_deferred' methods.
+
+class_name CallableDoubler
+
+
+const doubler_script :Script = preload("res://addons/gdUnit4/src/doubler/CallableDoubler.gd")
+
+var _cb: Callable
+
+
+func _init(cb: Callable) -> void:
+ assert(cb!=null, "Invalid argument <cb> must not be null")
+ _cb = cb
+
+## --- helpers -----------------------------------------------------------------------------------------------------------------------------
+static func map_func_name(method_info: Dictionary) -> String:
+ return method_info["name"]
+
+
+## We do not want to double all functions based on Object for this class
+## Is used on SpyBuilder to excluding functions to be doubled for Callable
+static func excluded_functions() -> PackedStringArray:
+ return ClassDB.class_get_method_list("Object")\
+ .map(CallableDoubler.map_func_name)\
+ .filter(func (name: String) -> bool:
+ return !CallableDoubler.callable_functions().has(name))
+
+
+static func non_callable_functions(name: String) -> bool:
+ return ![
+ # we allow "_init", is need to construct it,
+ "excluded_functions",
+ "non_callable_functions",
+ "callable_functions",
+ "map_func_name"
+ ].has(name)
+
+
+## Returns the list of supported Callable functions
+static func callable_functions() -> PackedStringArray:
+ var supported_functions :Array = doubler_script.get_script_method_list()\
+ .map(CallableDoubler.map_func_name)\
+ .filter(CallableDoubler.non_callable_functions)
+ # We manually add these functions that we cannot/may not overwrite in this class
+ supported_functions.append_array(["call_deferred", "callv"])
+ return supported_functions
+
+
+## -----------------------------------------------------------------------------------------------------------------------------------------
+## Callable functions stubing
+## -----------------------------------------------------------------------------------------------------------------------------------------
+
+func bind(...varargs: Array) -> Callable:
+ _cb = _cb.bindv(varargs)
+ return _cb
+
+
+func bindv(caller_args: Array) -> Callable:
+ _cb = _cb.bindv(caller_args)
+ return _cb
+
+
+@warning_ignore("native_method_override")
+func call(...varargs: Array) -> Variant:
+ return _cb.callv(varargs)
+
+
+# Is not supported, see class description
+#func call_deferred(...varargs: Array) -> void:
+# return _cb.call_deferred(varargs)
+
+
+# Is not supported, see class description
+#func callv(arguments: Array) -> Variant:
+# return _cb.callv(arguments)
+
+
+func get_bound_arguments() -> Array:
+ return _cb.get_bound_arguments()
+
+
+func get_bound_arguments_count() -> int:
+ return _cb.get_bound_arguments_count()
+
+
+func get_method() -> StringName:
+ return _cb.get_method()
+
+
+func get_object() -> Object:
+ return _cb.get_object()
+
+
+func get_object_id() -> int:
+ return _cb.get_object_id()
+
+
+func hash() -> int:
+ return _cb.hash()
+
+
+func is_custom() -> bool:
+ return _cb.is_custom()
+
+
+func is_null() -> bool:
+ return _cb.is_null()
+
+
+func is_standard() -> bool:
+ return _cb.is_standard()
+
+
+func is_valid() -> bool:
+ return _cb.is_valid()
+
+
+func rpc(...varargs: Array) -> void:
+ match varargs.size():
+ 0: _cb.rpc()
+ 1: _cb.rpc(varargs[0])
+ 2: _cb.rpc(varargs[0], varargs[1])
+ 3: _cb.rpc(varargs[0], varargs[1], varargs[2])
+ 4: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4])
+ 5: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5])
+ 6: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6])
+ 7: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7])
+ 8: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8])
+ 9: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9])
+
+
+@warning_ignore("untyped_declaration")
+func rpc_id(peer_id: int, ...varargs: Array) -> void:
+ match varargs.size():
+ 0: _cb.rpc_id(peer_id )
+ 1: _cb.rpc_id(peer_id, varargs[0])
+ 2: _cb.rpc_id(peer_id, varargs[0], varargs[1])
+ 3: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2])
+ 4: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4])
+ 5: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5])
+ 6: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6])
+ 7: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7])
+ 8: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8])
+ 9: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9])
+
+func unbind(argcount: int) -> Callable:
+ _cb = _cb.unbind(argcount)
+ return _cb
diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid
new file mode 100644
index 0000000..048f196
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid
@@ -0,0 +1 @@
+uid://dhp64cdhym7sd
diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd
new file mode 100644
index 0000000..d9459dc
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd
@@ -0,0 +1,4 @@
+@abstract class_name GdFunctionDoubler
+extends RefCounted
+
+@abstract func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray
diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid
new file mode 100644
index 0000000..ebaa0a1
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid
@@ -0,0 +1 @@
+uid://bydl05g1nfonb
diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd
new file mode 100644
index 0000000..31aa06b
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd
@@ -0,0 +1,119 @@
+# A class doubler used to mock and spy checked implementations
+class_name GdUnitClassDoubler
+extends RefCounted
+
+const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_"
+const EXCLUDE_VIRTUAL_FUNCTIONS = [
+ # we have to exclude notifications because NOTIFICATION_PREDELETE is try
+ # to delete already freed spy/mock resources and will result in a conflict
+ "_notification",
+ "notification",
+ # https://github.com/godotengine/godot/issues/67461
+ "get_name",
+ "get_path",
+ "duplicate",
+ ]
+# define functions to be exclude when spy or mock checked a scene
+const EXLCUDE_SCENE_FUNCTIONS = [
+ # needs to exclude get/set script functions otherwise it endsup in recursive endless loop
+ "set_script",
+ "get_script",
+ # needs to exclude otherwise verify fails checked collection arguments checked calling to string
+ "_to_string",
+]
+const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"]
+
+
+static func check_leaked_instances() -> void:
+ ## we check that all registered spy/mock instances are removed from the engine meta data
+ for key in Engine.get_meta_list():
+ if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX):
+ var instance: Variant = Engine.get_meta(key)
+ push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS])
+ await (Engine.get_main_loop() as SceneTree).process_frame
+
+
+# loads the doubler template
+# class_info = { "class_name": <>, "class_path" : <>}
+static func load_template(template: String, class_info: Dictionary) -> PackedStringArray:
+ var clazz_name: String = class_info.get("class_name")
+ var source_code := template\
+ .replace("${source_class}", clazz_name)\
+ # Replace template class_name DoubledClass<class> with source class name
+ .replace("SourceClassName", clazz_name.replace(".", "_"))
+ var lines := GdScriptParser.to_unix_format(source_code).split("\n")
+ @warning_ignore("return_value_discarded")
+ lines.insert(1, extends_clazz(class_info))
+ lines.insert(0, "@warning_ignore_start('unsafe_call_argument', 'shadowed_variable', 'untyped_declaration', 'native_method_override', 'int_as_enum_without_cast')")
+ return lines
+
+
+static func extends_clazz(class_info: Dictionary) -> String:
+ var clazz_name: String = class_info.get("class_name")
+ var clazz_path: PackedStringArray = class_info.get("class_path", [])
+ # is inner class?
+ if clazz_path.size() > 1:
+ return "extends %s" % clazz_name
+ if clazz_path.size() == 1 and clazz_path[0].ends_with(".gd"):
+ return "extends '%s'" % clazz_path[0]
+ return "extends %s" % clazz_name
+
+
+# double all functions of given instance
+static func double_functions(instance: Object, clazz_name: String, clazz_path: PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions: Array) -> PackedStringArray:
+ var doubled_source := PackedStringArray()
+ var parser := GdScriptParser.new()
+ var exclude_override_functions := EXCLUDE_VIRTUAL_FUNCTIONS + EXCLUDE_FUNCTIONS + exclude_functions
+ var functions := Array()
+
+ # double script functions
+ if not ClassDB.class_exists(clazz_name):
+ var result := parser.parse(clazz_name, clazz_path)
+ if result.is_error():
+ push_error(result.error_message())
+ return PackedStringArray()
+ var class_descriptor: GdClassDescriptor = result.value()
+ for func_descriptor in class_descriptor.functions():
+ if instance != null and not instance.has_method(func_descriptor.name()):
+ #prints("no virtual func implemented",clazz_name, func_descriptor.name() )
+ continue
+ if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()):
+ continue
+ doubled_source += func_doubler.double(func_descriptor)
+ functions.append(func_descriptor.name())
+
+ # double regular class functions
+ var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path)
+ for method: Dictionary in clazz_functions:
+ var func_descriptor := GdFunctionDescriptor.extract_from(method)
+ # exclude private core functions
+ if func_descriptor.is_private():
+ continue
+ if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()):
+ continue
+ # GD-110: Hotfix do not double invalid engine functions
+ if is_invalid_method_descriptior(method):
+ #prints("'%s': invalid method descriptor found! %s" % [clazz_name, method])
+ continue
+ # do not double on not implemented virtual functions
+ if instance != null and not instance.has_method(func_descriptor.name()):
+ #prints("no virtual func implemented",clazz_name, func_descriptor.name() )
+ continue
+ functions.append(func_descriptor.name())
+ doubled_source.append_array(func_doubler.double(func_descriptor))
+ return doubled_source
+
+
+# GD-110
+static func is_invalid_method_descriptior(method: Dictionary) -> bool:
+ var return_info: Dictionary = method["return"]
+ var type: int = return_info["type"]
+ var usage: int = return_info["usage"]
+ var clazz_name: String = return_info["class_name"]
+ # is method returning a type int with a given 'class_name' we have an enum
+ # and the PROPERTY_USAGE_CLASS_IS_ENUM must be set
+ if type == TYPE_INT and not clazz_name.is_empty() and not (usage & PROPERTY_USAGE_CLASS_IS_ENUM):
+ return true
+ if clazz_name == "Variant.Type":
+ return true
+ return false
diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid
new file mode 100644
index 0000000..05ce336
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid
@@ -0,0 +1 @@
+uid://b3nwjwttqye8a
diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd
new file mode 100644
index 0000000..a1a75f1
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd
@@ -0,0 +1,336 @@
+class_name GdUnitFunctionDoublerBuilder
+extends RefCounted
+
+const TYPE_VOID = GdObjects.TYPE_VOID
+const TYPE_VARIANT = GdObjects.TYPE_VARIANT
+const TYPE_VARARG = GdObjects.TYPE_VARARG
+const TYPE_FUNC = GdObjects.TYPE_FUNC
+const TYPE_FUZZER = GdObjects.TYPE_FUZZER
+const TYPE_ENUM = GdObjects.TYPE_ENUM
+
+const DEFAULT_TYPED_RETURN_VALUES := {
+ TYPE_NIL: "null",
+ TYPE_BOOL: "false",
+ TYPE_INT: "0",
+ TYPE_FLOAT: "0.0",
+ TYPE_STRING: "\"\"",
+ TYPE_STRING_NAME: "&\"\"",
+ TYPE_VECTOR2: "Vector2.ZERO",
+ TYPE_VECTOR2I: "Vector2i.ZERO",
+ TYPE_RECT2: "Rect2()",
+ TYPE_RECT2I: "Rect2i()",
+ TYPE_VECTOR3: "Vector3.ZERO",
+ TYPE_VECTOR3I: "Vector3i.ZERO",
+ TYPE_VECTOR4: "Vector4.ZERO",
+ TYPE_VECTOR4I: "Vector4i.ZERO",
+ TYPE_TRANSFORM2D: "Transform2D()",
+ TYPE_PLANE: "Plane()",
+ TYPE_QUATERNION: "Quaternion()",
+ TYPE_AABB: "AABB()",
+ TYPE_BASIS: "Basis()",
+ TYPE_TRANSFORM3D: "Transform3D()",
+ TYPE_PROJECTION: "Projection()",
+ TYPE_COLOR: "Color()",
+ TYPE_NODE_PATH: "NodePath()",
+ TYPE_RID: "RID()",
+ TYPE_OBJECT: "null",
+ TYPE_CALLABLE: "Callable()",
+ TYPE_SIGNAL: "Signal()",
+ TYPE_DICTIONARY: "Dictionary()",
+ TYPE_ARRAY: "Array()",
+ TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()",
+ TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()",
+ TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()",
+ TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()",
+ TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()",
+ TYPE_PACKED_STRING_ARRAY: "PackedStringArray()",
+ TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()",
+ TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()",
+ TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array()",
+ TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()",
+ GdObjects.TYPE_VARIANT: "null",
+ GdObjects.TYPE_ENUM: "0"
+}
+
+
+# @GlobalScript enums
+# needs to manually map because of https://github.com/godotengine/godot/issues/73835
+const DEFAULT_ENUM_RETURN_VALUES = {
+ "Side" : "SIDE_LEFT",
+ "Corner" : "CORNER_TOP_LEFT",
+ "Orientation" : "HORIZONTAL",
+ "ClockDirection" : "CLOCKWISE",
+ "HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT",
+ "VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP",
+ "InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO",
+ "EulerOrder" : "EULER_ORDER_XYZ",
+ "Key" : "KEY_NONE",
+ "KeyModifierMask" : "KEY_CODE_MASK",
+ "MouseButton" : "MOUSE_BUTTON_NONE",
+ "MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT",
+ "JoyButton" : "JOY_BUTTON_INVALID",
+ "JoyAxis" : "JOY_AXIS_INVALID",
+ "MIDIMessage" : "MIDI_MESSAGE_NONE",
+ "Error" : "OK",
+ "PropertyHint" : "PROPERTY_HINT_NONE",
+ "Variant.Type" : "TYPE_NIL",
+ "Vector2.Axis" : "Vector2.AXIS_X",
+ "Vector2i.Axis" : "Vector2i.AXIS_X",
+ "Vector3.Axis" : "Vector3.AXIS_X",
+ "Vector3i.Axis" : "Vector3i.AXIS_X",
+ "Vector4.Axis" : "Vector4.AXIS_X",
+ "Vector4i.Axis" : "Vector4i.AXIS_X",
+}
+
+
+static var def_constructor := """
+ func _init({constructor_args}) -> void:
+ __init_doubler()
+ super({args})
+ """.dedent()
+
+
+static var def_verify_block := """
+ # verify block
+ var __verifier := __get_verifier()
+ if __verifier != null:
+ if __verifier.is_verify_interactions():
+ __verifier.verify_interactions("{func_name}", __args)
+ {default_return}
+ else:
+ __verifier.save_function_interaction("{func_name}", __args)
+ """.dedent().indent("\t").trim_suffix("\n")
+
+
+static var def_prepare_block := """
+ if __is_prepare_return_value():
+ __save_function_return_value("{func_name}", __args)
+ {default_return}
+ """.dedent().indent("\t").trim_suffix("\n")
+
+
+static var def_void_prepare_block := """
+ if __is_prepare_return_value():
+ push_error("Mocking functions with return type void is not allowed!")
+ return
+ """.dedent().indent("\t").trim_suffix("\n")
+
+
+static var def_mock_return := """
+ if __is_do_not_call_real_func("{func_name}", __args):
+ return __return_mock_value("{func_name}", __args, {default_return})
+ """.dedent().indent("\t").trim_suffix("\n")
+
+
+static var def_void_mock_return := """
+ if __is_do_not_call_real_func("{func_name}", __args):
+ return
+ """.dedent().indent("\t").trim_suffix("\n")
+
+
+var fd: GdFunctionDescriptor
+var func_args: Array
+var default_return: String
+var verify_block: String = ""
+var prepare_block: String = ""
+var mock_return: String = ""
+
+
+func _init(descriptor: GdFunctionDescriptor) -> void:
+ # verify all default types are covered
+ for type_key in TYPE_MAX:
+ if not DEFAULT_TYPED_RETURN_VALUES.has(type_key):
+ push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX])
+ prints("missing default definition for type", type_key)
+ assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!")
+
+ fd = descriptor
+ func_args = argument_names()
+ default_return = default_return_value()
+
+
+func build_func_signature() -> String:
+ var return_type := ":" if fd._return_type == TYPE_VARIANT else " -> %s:" % fd.return_type_as_string()
+ return "{static}func {func_name}({args}){return_type}".format({
+ "static" : "static " if fd.is_static() else "",
+ "func_name": fd.name(),
+ "args": arguments_full_quilified(),
+ "return_type": return_type
+ })
+
+
+func arguments_full_quilified() -> String:
+ var collect := PackedStringArray()
+ for arg in fd.args():
+ var name := argument_name(arg)
+ if arg.has_default():
+ var signature := "{argument_name}{arg_typed}={arg_value}".format({
+ "argument_name" : name,
+ "arg_typed" : ":"+GdObjects.type_as_string(arg.type()) if arg.type() == GdObjects.TYPE_VARIANT else "",
+ "arg_value" : arg.value_as_string()
+ })
+ collect.push_back(signature)
+ else:
+ collect.push_back(name)
+ if fd.is_vararg():
+ var arg_descriptor := fd.varargs()[0]
+ collect.push_back("...%s_: Array" % arg_descriptor.name())
+ return ", ".join(collect)
+
+
+func argument_name(arg: GdFunctionArgument) -> String:
+ return arg.name() + "_"
+
+
+func argument_names() -> PackedStringArray:
+ return fd.args().map(argument_name)
+
+
+func argument_default(arg :GdFunctionArgument) -> String:
+ return (arg.value_as_string()
+ if arg.has_default()
+ else DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null"))
+
+
+func build_constructor_arguments() -> String:
+ var arguments := PackedStringArray()
+ for arg in fd.args():
+ var default_value := argument_default(arg)
+ var arg_signature := "{name}:{type}={default}".format({
+ "name" : argument_name(arg),
+ "type" : "Variant" if default_value == "null" else "",
+ "default" : default_value
+ })
+ arguments.append(arg_signature)
+ if fd.is_vararg():
+ arguments.append("...varargs: Array")
+ return ", ".join(arguments)
+
+
+func build_arguments() -> String:
+ return "\tvar __args := [{args}]{varargs}".format({
+ "args" : ", ".join(func_args),
+ "varargs" : " + varargs_" if fd.is_vararg() else ""
+ })
+
+
+func build_super_calls() -> String:
+ if !fd.is_vararg():
+ return 'super(%s)\n' % ", ".join(func_args)
+
+ var match_block := "match varargs_.size():\n"
+ for index in range(0, 11):
+ match_block += '{index}: super({args})\n'.format({
+ "index" : index,
+ "args" : ", ".join(func_args + build_vararg_list(index))
+ }).indent("\t")
+ match_block += '_: push_error("To many varradic arguments.")\n'.indent("\t")
+ match_block += "return\n" if is_void_func() else "return %s\n" % default_return
+ return match_block
+
+
+func build_vararg_list(count: int) -> Array:
+ var arg_list := []
+ for index in count:
+ arg_list.append("varargs_[%d]" % index)
+ return arg_list
+
+
+func default_return_value() -> String:
+ var return_type: Variant = fd.return_type()
+ if return_type == GdObjects.TYPE_ENUM:
+ var enum_class := fd._return_class
+ if DEFAULT_ENUM_RETURN_VALUES.has(enum_class):
+ return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0")
+
+ var enum_path := enum_class.split(".")
+ if enum_path.size() >= 2:
+ var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1])
+ if not keys.is_empty():
+ return "%s.%s" % [enum_path[0], keys[0]]
+ var enum_value: Variant = get_enum_default(enum_class)
+ if enum_value != null:
+ return str(enum_value)
+ # we need fallback for @GlobalScript enums,
+ return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0")
+ return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid")
+
+
+# Determine the enum default by reflection
+func get_enum_default(value: String) -> Variant:
+ var script := GDScript.new()
+ script.source_code = """
+ extends RefCounted
+
+ static func get_enum_default() -> Variant:
+ return %s.values()[0]
+
+ """.dedent() % value
+ var err := script.reload()
+ if err != OK:
+ push_error("Cant get enum values form '%s', %s" % [value, error_string(err)])
+ return 0
+ @warning_ignore("unsafe_method_access")
+ return script.new().call("get_enum_default")
+
+
+func is_void_func() -> bool:
+ return fd.return_type() == TYPE_NIL or fd.return_type() == TYPE_VOID
+
+
+func with_verify_block() -> GdUnitFunctionDoublerBuilder:
+ verify_block = def_verify_block.format({
+ "func_name" : fd.name(),
+ "default_return" : "return" if is_void_func() else "return " + default_return
+ })
+ return self
+
+
+func with_prepare_block() -> GdUnitFunctionDoublerBuilder:
+ if fd.return_type() == TYPE_NIL or fd.return_type() == GdObjects.TYPE_VOID:
+ prepare_block = def_void_prepare_block
+ return self
+
+ prepare_block = def_prepare_block.format({
+ "func_name" : fd.name(),
+ "default_return" : "return" if is_void_func() else "return " + default_return
+ })
+ return self
+
+
+func with_mocked_return_value() -> GdUnitFunctionDoublerBuilder:
+ if is_void_func():
+ mock_return = def_void_mock_return.format({
+ "func_name" : fd.name(),
+ })
+ else:
+ mock_return = def_mock_return.format({
+ "func_name" : fd.name(),
+ "default_return" : '"no_arg"' if is_void_func() else default_return
+ })
+ return self
+
+
+func build() -> PackedStringArray:
+ if fd.name() == "_init":
+ return [def_constructor.format({
+ "constructor_args" : build_constructor_arguments(),
+ "args" : ", ".join(func_args)
+ })]
+
+ var func_body: PackedStringArray = []
+ func_body.append(build_func_signature())
+ func_body.append(build_arguments())
+ if not prepare_block.is_empty():
+ func_body.append(prepare_block)
+ func_body.append(verify_block)
+ if not mock_return.is_empty():
+ func_body.append(mock_return)
+ func_body.append("")
+ var super_calls := build_super_calls()
+ if not is_void_func():
+ super_calls = super_calls.replace("super(", "return super(" )
+ if fd.is_coroutine():
+ super_calls = super_calls.replace("super(", "await super(" )
+ func_body.append(super_calls.indent("\t"))
+ return func_body
diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid
new file mode 100644
index 0000000..ca2ad79
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid
@@ -0,0 +1 @@
+uid://bx657fdodkwc6
diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd
new file mode 100644
index 0000000..d735bc5
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd
@@ -0,0 +1,10 @@
+class_name GdUnitMockFunctionDoubler
+extends GdFunctionDoubler
+
+
+func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray:
+ return GdUnitFunctionDoublerBuilder.new(func_descriptor)\
+ .with_prepare_block()\
+ .with_verify_block()\
+ .with_mocked_return_value()\
+ .build()
diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid
new file mode 100644
index 0000000..4117f4e
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid
@@ -0,0 +1 @@
+uid://teng0fasbhen
diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd
new file mode 100644
index 0000000..789eb32
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd
@@ -0,0 +1,53 @@
+class_name GdUnitObjectInteractions
+extends RefCounted
+
+
+static func verify(interaction_object: Object, interactions_times: int) -> Variant:
+ if not _is_mock_or_spy(interaction_object):
+ return interaction_object
+
+ _get_verifier(interaction_object).do_verify_interactions(interactions_times)
+ return interaction_object
+
+
+static func verify_no_interactions(interaction_object: Object) -> GdUnitAssert:
+ var assert_tool := GdUnitAssertImpl.new("")
+ if not _is_mock_or_spy(interaction_object):
+ return assert_tool.report_success()
+
+ var summary := _get_verifier(interaction_object).verify_no_interactions()
+ if summary.is_empty():
+ return assert_tool.report_success()
+ return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary))
+
+
+static func verify_no_more_interactions(interaction_object: Object) -> GdUnitAssert:
+ var assert_tool := GdUnitAssertImpl.new("")
+ if not _is_mock_or_spy(interaction_object):
+ return assert_tool
+
+ var summary := _get_verifier(interaction_object).verify_no_more_interactions()
+ if summary.is_empty():
+ return assert_tool
+ return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary))
+
+
+static func reset(interaction_object: Object) -> Object:
+ if not _is_mock_or_spy(interaction_object):
+ return interaction_object
+
+ _get_verifier(interaction_object).reset_interactions()
+ return interaction_object
+
+
+static func _is_mock_or_spy(instance: Object) -> bool:
+ if instance != null and instance.has_method("__get_verifier"):
+ return true
+
+ push_error("Error: The given object '%s' is not a mock or spy instance!" % instance)
+ return false
+
+
+static func _get_verifier(interaction_object: Object) -> GdUnitObjectInteractionsVerifier:
+ @warning_ignore("unsafe_method_access")
+ return interaction_object.__get_verifier()
diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid
new file mode 100644
index 0000000..54ed63f
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid
@@ -0,0 +1 @@
+uid://cf4wbrdcfcsap
diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd
new file mode 100644
index 0000000..36aa978
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd
@@ -0,0 +1,84 @@
+class_name GdUnitObjectInteractionsVerifier
+
+var expected_interactions: int = -1
+var saved_interactions := Dictionary()
+var verified_interactions := Array()
+
+
+func save_function_interaction(func_name: String, args :Array[Variant]) -> void:
+ var function_args := [func_name] + args
+ var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true)
+ for index in saved_interactions.keys().size():
+ var key: Variant = saved_interactions.keys()[index]
+ if matcher.is_match(key):
+ saved_interactions[key] += 1
+ return
+ saved_interactions[function_args] = 1
+
+
+func is_verify_interactions() -> bool:
+ return expected_interactions != -1
+
+
+func do_verify_interactions(interactions_times: int = 1) -> void:
+ expected_interactions = interactions_times
+
+
+func verify_interactions(func_name: String, args: Array[Variant]) -> void:
+ var summary := Dictionary()
+ var total_interactions := 0
+ var function_args := [func_name] + args
+ var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true)
+ for index in saved_interactions.keys().size():
+ var key: Variant = saved_interactions.keys()[index]
+ if matcher.is_match(key):
+ var interactions: int = saved_interactions.get(key, 0)
+ total_interactions += interactions
+ summary[key] = interactions
+ # add as verified
+ verified_interactions.append(key)
+
+ var assert_tool := GdUnitAssertImpl.new("")
+ if total_interactions != expected_interactions:
+ var __expected_summary := {function_args : expected_interactions}
+ var error_message: String
+ # if no interactions macht collect not verified interactions for failure report
+ if summary.is_empty():
+ var __current_summary := verify_no_more_interactions()
+ error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary)
+ else:
+ error_message = GdAssertMessages.error_validate_interactions(summary, __expected_summary)
+ @warning_ignore("return_value_discarded")
+ assert_tool.report_error(error_message)
+ else:
+ @warning_ignore("return_value_discarded")
+ assert_tool.report_success()
+ expected_interactions = -1
+
+
+func verify_no_interactions() -> Dictionary:
+ var summary := Dictionary()
+ if not saved_interactions.is_empty():
+ for index in saved_interactions.keys().size():
+ var func_call: Variant = saved_interactions.keys()[index]
+ summary[func_call] = saved_interactions[func_call]
+ return summary
+
+
+func verify_no_more_interactions() -> Dictionary:
+ var summary := Dictionary()
+ var called_functions: Array[Variant] = saved_interactions.keys()
+ if called_functions != verified_interactions:
+ # collect the not verified functions
+ var called_but_not_verified := called_functions.duplicate()
+ for index in verified_interactions.size():
+ called_but_not_verified.erase(verified_interactions[index])
+
+ for index in called_but_not_verified.size():
+ var not_verified: Variant = called_but_not_verified[index]
+ summary[not_verified] = saved_interactions[not_verified]
+ return summary
+
+
+func reset_interactions() -> void:
+ saved_interactions.clear()
diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid
new file mode 100644
index 0000000..9b42fe6
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid
@@ -0,0 +1 @@
+uid://djphb0prfq8if
diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd
new file mode 100644
index 0000000..091241d
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd
@@ -0,0 +1,8 @@
+class_name GdUnitSpyFunctionDoubler
+extends GdFunctionDoubler
+
+
+func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray:
+ return GdUnitFunctionDoublerBuilder.new(func_descriptor)\
+ .with_verify_block()\
+ .build()
diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid
new file mode 100644
index 0000000..322f6c9
--- /dev/null
+++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid
@@ -0,0 +1 @@
+uid://3rjtjcj64vpt