686 lines
22 KiB
GDScript
686 lines
22 KiB
GDScript
# This is a helper class to compare two objects by equals
|
|
class_name GdObjects
|
|
extends Resource
|
|
|
|
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
|
|
|
const TYPE_VOID = TYPE_MAX + 1000
|
|
const TYPE_VARARG = TYPE_MAX + 1001
|
|
const TYPE_VARIANT = TYPE_MAX + 1002
|
|
const TYPE_FUNC = TYPE_MAX + 1003
|
|
const TYPE_FUZZER = TYPE_MAX + 1004
|
|
|
|
const TYPE_NODE = TYPE_MAX + 2001
|
|
# missing Godot types
|
|
const TYPE_CONTROL = TYPE_MAX + 2002
|
|
const TYPE_CANVAS = TYPE_MAX + 2003
|
|
const TYPE_ENUM = TYPE_MAX + 2004
|
|
|
|
|
|
# used as default value for varargs
|
|
const TYPE_VARARG_PLACEHOLDER_VALUE = "__null__"
|
|
|
|
|
|
const TYPE_AS_STRING_MAPPINGS := {
|
|
TYPE_NIL: "null",
|
|
TYPE_BOOL: "bool",
|
|
TYPE_INT: "int",
|
|
TYPE_FLOAT: "float",
|
|
TYPE_STRING: "String",
|
|
TYPE_VECTOR2: "Vector2",
|
|
TYPE_VECTOR2I: "Vector2i",
|
|
TYPE_RECT2: "Rect2",
|
|
TYPE_RECT2I: "Rect2i",
|
|
TYPE_VECTOR3: "Vector3",
|
|
TYPE_VECTOR3I: "Vector3i",
|
|
TYPE_TRANSFORM2D: "Transform2D",
|
|
TYPE_VECTOR4: "Vector4",
|
|
TYPE_VECTOR4I: "Vector4i",
|
|
TYPE_PLANE: "Plane",
|
|
TYPE_QUATERNION: "Quaternion",
|
|
TYPE_AABB: "AABB",
|
|
TYPE_BASIS: "Basis",
|
|
TYPE_TRANSFORM3D: "Transform3D",
|
|
TYPE_PROJECTION: "Projection",
|
|
TYPE_COLOR: "Color",
|
|
TYPE_STRING_NAME: "StringName",
|
|
TYPE_NODE_PATH: "NodePath",
|
|
TYPE_RID: "RID",
|
|
TYPE_OBJECT: "Object",
|
|
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_COLOR_ARRAY: "PackedColorArray",
|
|
TYPE_VOID: "void",
|
|
TYPE_VARARG: "VarArg",
|
|
TYPE_FUNC: "Func",
|
|
TYPE_FUZZER: "Fuzzer",
|
|
TYPE_VARIANT: "Variant"
|
|
}
|
|
|
|
|
|
const NOTIFICATION_AS_STRING_MAPPINGS := {
|
|
TYPE_OBJECT: {
|
|
Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE",
|
|
Object.NOTIFICATION_PREDELETE: "PREDELETE",
|
|
EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED",
|
|
},
|
|
TYPE_NODE: {
|
|
Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE",
|
|
Node.NOTIFICATION_EXIT_TREE: "EXIT_TREE",
|
|
Node.NOTIFICATION_MOVED_IN_PARENT: "MOVED_IN_PARENT",
|
|
Node.NOTIFICATION_READY: "READY",
|
|
Node.NOTIFICATION_PAUSED: "PAUSED",
|
|
Node.NOTIFICATION_UNPAUSED: "UNPAUSED",
|
|
Node.NOTIFICATION_PHYSICS_PROCESS: "PHYSICS_PROCESS",
|
|
Node.NOTIFICATION_PROCESS: "PROCESS",
|
|
Node.NOTIFICATION_PARENTED: "PARENTED",
|
|
Node.NOTIFICATION_UNPARENTED: "UNPARENTED",
|
|
Node.NOTIFICATION_SCENE_INSTANTIATED: "INSTANCED",
|
|
Node.NOTIFICATION_DRAG_BEGIN: "DRAG_BEGIN",
|
|
Node.NOTIFICATION_DRAG_END: "DRAG_END",
|
|
Node.NOTIFICATION_PATH_RENAMED: "PATH_CHANGED",
|
|
Node.NOTIFICATION_INTERNAL_PROCESS: "INTERNAL_PROCESS",
|
|
Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS: "INTERNAL_PHYSICS_PROCESS",
|
|
Node.NOTIFICATION_POST_ENTER_TREE: "POST_ENTER_TREE",
|
|
Node.NOTIFICATION_WM_MOUSE_ENTER: "WM_MOUSE_ENTER",
|
|
Node.NOTIFICATION_WM_MOUSE_EXIT: "WM_MOUSE_EXIT",
|
|
Node.NOTIFICATION_APPLICATION_FOCUS_IN: "WM_FOCUS_IN",
|
|
Node.NOTIFICATION_APPLICATION_FOCUS_OUT: "WM_FOCUS_OUT",
|
|
#Node.NOTIFICATION_WM_QUIT_REQUEST: "WM_QUIT_REQUEST",
|
|
Node.NOTIFICATION_WM_GO_BACK_REQUEST: "WM_GO_BACK_REQUEST",
|
|
Node.NOTIFICATION_WM_WINDOW_FOCUS_OUT: "WM_UNFOCUS_REQUEST",
|
|
Node.NOTIFICATION_OS_MEMORY_WARNING: "OS_MEMORY_WARNING",
|
|
Node.NOTIFICATION_TRANSLATION_CHANGED: "TRANSLATION_CHANGED",
|
|
Node.NOTIFICATION_WM_ABOUT: "WM_ABOUT",
|
|
Node.NOTIFICATION_CRASH: "CRASH",
|
|
Node.NOTIFICATION_OS_IME_UPDATE: "OS_IME_UPDATE",
|
|
Node.NOTIFICATION_APPLICATION_RESUMED: "APP_RESUMED",
|
|
Node.NOTIFICATION_APPLICATION_PAUSED: "APP_PAUSED",
|
|
Node3D.NOTIFICATION_TRANSFORM_CHANGED: "TRANSFORM_CHANGED",
|
|
Node3D.NOTIFICATION_ENTER_WORLD: "ENTER_WORLD",
|
|
Node3D.NOTIFICATION_EXIT_WORLD: "EXIT_WORLD",
|
|
Node3D.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED",
|
|
Skeleton3D.NOTIFICATION_UPDATE_SKELETON: "UPDATE_SKELETON",
|
|
CanvasItem.NOTIFICATION_DRAW: "DRAW",
|
|
CanvasItem.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED",
|
|
CanvasItem.NOTIFICATION_ENTER_CANVAS: "ENTER_CANVAS",
|
|
CanvasItem.NOTIFICATION_EXIT_CANVAS: "EXIT_CANVAS",
|
|
#Popup.NOTIFICATION_POST_POPUP: "POST_POPUP",
|
|
#Popup.NOTIFICATION_POPUP_HIDE: "POPUP_HIDE",
|
|
},
|
|
TYPE_CONTROL : {
|
|
Container.NOTIFICATION_SORT_CHILDREN: "SORT_CHILDREN",
|
|
Control.NOTIFICATION_RESIZED: "RESIZED",
|
|
Control.NOTIFICATION_MOUSE_ENTER: "MOUSE_ENTER",
|
|
Control.NOTIFICATION_MOUSE_EXIT: "MOUSE_EXIT",
|
|
Control.NOTIFICATION_FOCUS_ENTER: "FOCUS_ENTER",
|
|
Control.NOTIFICATION_FOCUS_EXIT: "FOCUS_EXIT",
|
|
Control.NOTIFICATION_THEME_CHANGED: "THEME_CHANGED",
|
|
#Control.NOTIFICATION_MODAL_CLOSE: "MODAL_CLOSE",
|
|
Control.NOTIFICATION_SCROLL_BEGIN: "SCROLL_BEGIN",
|
|
Control.NOTIFICATION_SCROLL_END: "SCROLL_END",
|
|
}
|
|
}
|
|
|
|
|
|
enum COMPARE_MODE {
|
|
OBJECT_REFERENCE,
|
|
PARAMETER_DEEP_TEST
|
|
}
|
|
|
|
|
|
# prototype of better object to dictionary
|
|
static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary:
|
|
if obj == null:
|
|
return {}
|
|
var clazz_name := obj.get_class()
|
|
var dict := Dictionary()
|
|
var clazz_path := ""
|
|
|
|
if is_instance_valid(obj) and obj.get_script() != null:
|
|
var d := inst_to_dict(obj)
|
|
clazz_path = d["@path"]
|
|
if d["@subpath"] != NodePath(""):
|
|
clazz_name = d["@subpath"]
|
|
dict["@inner_class"] = true
|
|
else:
|
|
clazz_name = clazz_path.get_file().replace(".gd", "")
|
|
dict["@path"] = clazz_path
|
|
|
|
for property in obj.get_property_list():
|
|
var property_name = property["name"]
|
|
var property_type = property["type"]
|
|
var property_value = obj.get(property_name)
|
|
if property_value is GDScript or property_value is Callable:
|
|
continue
|
|
if (property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE|PROPERTY_USAGE_DEFAULT
|
|
and not property["usage"] & PROPERTY_USAGE_CATEGORY
|
|
and not property["usage"] == 0):
|
|
if property_type == TYPE_OBJECT:
|
|
# prevent recursion
|
|
if hashed_objects.has(obj):
|
|
dict[property_name] = str(property_value)
|
|
continue
|
|
hashed_objects[obj] = true
|
|
dict[property_name] = obj2dict(property_value, hashed_objects)
|
|
else:
|
|
dict[property_name] = property_value
|
|
return {"%s" % clazz_name : dict}
|
|
|
|
|
|
static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
|
|
return _equals(obj_a, obj_b, case_sensitive, compare_mode, [], 0)
|
|
|
|
|
|
static func equals_sorted(obj_a :Array, obj_b :Array, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
|
|
var a := obj_a.duplicate()
|
|
var b := obj_b.duplicate()
|
|
a.sort()
|
|
b.sort()
|
|
return equals(a, b, case_sensitive, compare_mode)
|
|
|
|
|
|
static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool:
|
|
var type_a := typeof(obj_a)
|
|
var type_b := typeof(obj_b)
|
|
if stack_depth > 32:
|
|
prints("stack_depth", stack_depth, deep_stack)
|
|
push_error("GdUnit equals has max stack deep reached!")
|
|
return false
|
|
|
|
# use argument matcher if requested
|
|
if is_instance_valid(obj_a) and obj_a is GdUnitArgumentMatcher:
|
|
return (obj_a as GdUnitArgumentMatcher).is_match(obj_b)
|
|
if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher:
|
|
return (obj_b as GdUnitArgumentMatcher).is_match(obj_a)
|
|
|
|
stack_depth += 1
|
|
# fast fail is different types
|
|
if not _is_type_equivalent(type_a, type_b):
|
|
return false
|
|
# is same instance
|
|
if obj_a == obj_b:
|
|
return true
|
|
# handle null values
|
|
if obj_a == null and obj_b != null:
|
|
return false
|
|
if obj_b == null and obj_a != null:
|
|
return false
|
|
|
|
match type_a:
|
|
TYPE_OBJECT:
|
|
if deep_stack.has(obj_a) or deep_stack.has(obj_b):
|
|
return true
|
|
deep_stack.append(obj_a)
|
|
deep_stack.append(obj_b)
|
|
if compare_mode == COMPARE_MODE.PARAMETER_DEEP_TEST:
|
|
# fail fast
|
|
if not is_instance_valid(obj_a) or not is_instance_valid(obj_b):
|
|
return false
|
|
if obj_a.get_class() != obj_b.get_class():
|
|
return false
|
|
var a = obj2dict(obj_a)
|
|
var b = obj2dict(obj_b)
|
|
return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth)
|
|
return obj_a == obj_b
|
|
|
|
TYPE_ARRAY:
|
|
if obj_a.size() != obj_b.size():
|
|
return false
|
|
for index in obj_a.size():
|
|
if not _equals(obj_a[index], obj_b[index], case_sensitive, compare_mode, deep_stack, stack_depth):
|
|
return false
|
|
return true
|
|
|
|
TYPE_DICTIONARY:
|
|
if obj_a.size() != obj_b.size():
|
|
return false
|
|
for key in obj_a.keys():
|
|
var value_a = obj_a[key] if obj_a.has(key) else null
|
|
var value_b = obj_b[key] if obj_b.has(key) else null
|
|
if not _equals(value_a, value_b, case_sensitive, compare_mode, deep_stack, stack_depth):
|
|
return false
|
|
return true
|
|
|
|
TYPE_STRING:
|
|
if case_sensitive:
|
|
return obj_a.to_lower() == obj_b.to_lower()
|
|
else:
|
|
return obj_a == obj_b
|
|
return obj_a == obj_b
|
|
|
|
|
|
@warning_ignore("shadowed_variable_base_class")
|
|
static func notification_as_string(instance :Variant, notification :int) -> String:
|
|
var error := "Unknown notification: '%s' at instance: %s" % [notification, instance]
|
|
if instance is Node:
|
|
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].get(notification, error)
|
|
if instance is Control:
|
|
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].get(notification, error)
|
|
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_OBJECT].get(notification, error)
|
|
|
|
|
|
static func string_to_type(value :String) -> int:
|
|
for type in TYPE_AS_STRING_MAPPINGS.keys():
|
|
if TYPE_AS_STRING_MAPPINGS.get(type) == value:
|
|
return type
|
|
return TYPE_NIL
|
|
|
|
|
|
static func to_camel_case(value :String) -> String:
|
|
var p := to_pascal_case(value)
|
|
if not p.is_empty():
|
|
p[0] = p[0].to_lower()
|
|
return p
|
|
|
|
|
|
static func to_pascal_case(value :String) -> String:
|
|
return value.capitalize().replace(" ", "")
|
|
|
|
|
|
static func to_snake_case(value :String) -> String:
|
|
var result = PackedStringArray()
|
|
for ch in value:
|
|
var lower_ch = ch.to_lower()
|
|
if ch != lower_ch and result.size() > 1:
|
|
result.append('_')
|
|
result.append(lower_ch)
|
|
return ''.join(result)
|
|
|
|
|
|
static func is_snake_case(value :String) -> bool:
|
|
for ch in value:
|
|
if ch == '_':
|
|
continue
|
|
if ch == ch.to_upper():
|
|
return false
|
|
return true
|
|
|
|
|
|
static func type_as_string(type :int) -> String:
|
|
return TYPE_AS_STRING_MAPPINGS.get(type, "Variant")
|
|
|
|
|
|
static func typeof_as_string(value :Variant) -> String:
|
|
return TYPE_AS_STRING_MAPPINGS.get(typeof(value), "Unknown type")
|
|
|
|
|
|
static func all_types() -> PackedInt32Array:
|
|
return PackedInt32Array(TYPE_AS_STRING_MAPPINGS.keys())
|
|
|
|
|
|
static func string_as_typeof(type_name :String) -> int:
|
|
var type :Variant = TYPE_AS_STRING_MAPPINGS.find_key(type_name)
|
|
return type if type != null else TYPE_VARIANT
|
|
|
|
|
|
static func is_primitive_type(value :Variant) -> bool:
|
|
return typeof(value) in [TYPE_BOOL, TYPE_STRING, TYPE_STRING_NAME, TYPE_INT, TYPE_FLOAT]
|
|
|
|
|
|
static func _is_type_equivalent(type_a :int, type_b :int) -> bool:
|
|
# don't test for TYPE_STRING_NAME equivalenz
|
|
if type_a == TYPE_STRING_NAME or type_b == TYPE_STRING_NAME:
|
|
return true
|
|
if GdUnitSettings.is_strict_number_type_compare():
|
|
return type_a == type_b
|
|
return (
|
|
(type_a == TYPE_FLOAT and type_b == TYPE_INT)
|
|
or (type_a == TYPE_INT and type_b == TYPE_FLOAT)
|
|
or type_a == type_b)
|
|
|
|
|
|
static func is_engine_type(value :Object) -> bool:
|
|
if value is GDScript or value is ScriptExtension:
|
|
return false
|
|
return value.is_class("GDScriptNativeClass")
|
|
|
|
|
|
static func is_type(value :Variant) -> bool:
|
|
# is an build-in type
|
|
if typeof(value) != TYPE_OBJECT:
|
|
return false
|
|
# is a engine class type
|
|
if is_engine_type(value):
|
|
return true
|
|
# is a custom class type
|
|
if value is GDScript and value.can_instantiate():
|
|
return true
|
|
return false
|
|
|
|
|
|
static func _is_same(left :Variant, right :Variant) -> bool:
|
|
var left_type := -1 if left == null else typeof(left)
|
|
var right_type := -1 if right == null else typeof(right)
|
|
|
|
# if typ different can't be the same
|
|
if left_type != right_type:
|
|
return false
|
|
if left_type == TYPE_OBJECT and right_type == TYPE_OBJECT:
|
|
return left.get_instance_id() == right.get_instance_id()
|
|
return equals(left, right)
|
|
|
|
|
|
static func is_object(value :Variant) -> bool:
|
|
return typeof(value) == TYPE_OBJECT
|
|
|
|
|
|
static func is_script(value :Variant) -> bool:
|
|
return is_object(value) and value is Script
|
|
|
|
|
|
static func is_test_suite(script :Script) -> bool:
|
|
return is_gd_testsuite(script) or GdUnit4CSharpApiLoader.is_test_suite(script.resource_path)
|
|
|
|
|
|
static func is_native_class(value :Variant) -> bool:
|
|
return is_object(value) and is_engine_type(value)
|
|
|
|
|
|
static func is_scene(value :Variant) -> bool:
|
|
return is_object(value) and value is PackedScene
|
|
|
|
|
|
static func is_scene_resource_path(value :Variant) -> bool:
|
|
return value is String and value.ends_with(".tscn")
|
|
|
|
|
|
static func is_gd_script(script :Script) -> bool:
|
|
return script is GDScript
|
|
|
|
|
|
static func is_cs_script(script :Script) -> bool:
|
|
# we need to check by stringify name because checked non mono Godot the class CSharpScript is not available
|
|
return str(script).find("CSharpScript") != -1
|
|
|
|
|
|
static func is_gd_testsuite(script :Script) -> bool:
|
|
if is_gd_script(script):
|
|
var stack := [script]
|
|
while not stack.is_empty():
|
|
var current := stack.pop_front() as Script
|
|
var base := current.get_base_script() as Script
|
|
if base != null:
|
|
if base.resource_path.find("GdUnitTestSuite") != -1:
|
|
return true
|
|
stack.push_back(base)
|
|
return false
|
|
|
|
|
|
static func is_singleton(value :Variant) -> bool:
|
|
if not is_instance_valid(value) or is_native_class(value):
|
|
return false
|
|
for name in Engine.get_singleton_list():
|
|
if value.is_class(name):
|
|
return true
|
|
return false
|
|
|
|
|
|
static func is_instance(value :Variant) -> bool:
|
|
if not is_instance_valid(value) or is_native_class(value):
|
|
return false
|
|
if is_script(value) and value.get_instance_base_type() == "":
|
|
return true
|
|
if is_scene(value):
|
|
return true
|
|
return not value.has_method('new') and not value.has_method('instance')
|
|
|
|
|
|
# only object form type Node and attached filename
|
|
static func is_instance_scene(instance :Variant) -> bool:
|
|
if instance is Node:
|
|
var node := instance as Node
|
|
return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty()
|
|
return false
|
|
|
|
|
|
static func can_be_instantiate(obj :Variant) -> bool:
|
|
if not obj or is_engine_type(obj):
|
|
return false
|
|
return obj.has_method("new")
|
|
|
|
|
|
static func create_instance(clazz :Variant) -> GdUnitResult:
|
|
match typeof(clazz):
|
|
TYPE_OBJECT:
|
|
# test is given clazz already an instance
|
|
if is_instance(clazz):
|
|
return GdUnitResult.success(clazz)
|
|
return GdUnitResult.success(clazz.new())
|
|
TYPE_STRING:
|
|
if ClassDB.class_exists(clazz):
|
|
if Engine.has_singleton(clazz):
|
|
return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz)
|
|
if not ClassDB.can_instantiate(clazz):
|
|
return GdUnitResult.error("Can't instance Engine class '%s'." % clazz)
|
|
return GdUnitResult.success(ClassDB.instantiate(clazz))
|
|
else:
|
|
var clazz_path :String = extract_class_path(clazz)[0]
|
|
if not FileAccess.file_exists(clazz_path):
|
|
return GdUnitResult.error("Class '%s' not found." % clazz)
|
|
var script := load(clazz_path)
|
|
if script != null:
|
|
return GdUnitResult.success(script.new())
|
|
else:
|
|
return GdUnitResult.error("Can't create instance for '%s'." % clazz)
|
|
return GdUnitResult.error("Can't create instance for class '%s'." % clazz)
|
|
|
|
|
|
static func extract_class_path(clazz :Variant) -> PackedStringArray:
|
|
var clazz_path := PackedStringArray()
|
|
if clazz is String:
|
|
clazz_path.append(clazz)
|
|
return clazz_path
|
|
if is_instance(clazz):
|
|
# is instance a script instance?
|
|
var script := clazz.script as GDScript
|
|
if script != null:
|
|
return extract_class_path(script)
|
|
return clazz_path
|
|
|
|
if clazz is GDScript:
|
|
if not clazz.resource_path.is_empty():
|
|
clazz_path.append(clazz.resource_path)
|
|
return clazz_path
|
|
# if not found we go the expensive way and extract the path form the script by creating an instance
|
|
var arg_list := build_function_default_arguments(clazz, "_init")
|
|
var instance = clazz.callv("new", arg_list)
|
|
var clazz_info := inst_to_dict(instance)
|
|
GdUnitTools.free_instance(instance)
|
|
clazz_path.append(clazz_info["@path"])
|
|
if clazz_info.has("@subpath"):
|
|
var sub_path :String = clazz_info["@subpath"]
|
|
if not sub_path.is_empty():
|
|
var sub_paths := sub_path.split("/")
|
|
clazz_path += sub_paths
|
|
return clazz_path
|
|
return clazz_path
|
|
|
|
|
|
static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> String:
|
|
var base_clazz := clazz_path[0]
|
|
# return original class name if engine class
|
|
if ClassDB.class_exists(base_clazz):
|
|
return base_clazz
|
|
var clazz_name := to_pascal_case(base_clazz.get_basename().get_file())
|
|
for path_index in range(1, clazz_path.size()):
|
|
clazz_name += "." + clazz_path[path_index]
|
|
return clazz_name
|
|
|
|
|
|
static func extract_class_name(clazz :Variant) -> GdUnitResult:
|
|
if clazz == null:
|
|
return GdUnitResult.error("Can't extract class name form a null value.")
|
|
|
|
if is_instance(clazz):
|
|
# is instance a script instance?
|
|
var script := clazz.script as GDScript
|
|
if script != null:
|
|
return extract_class_name(script)
|
|
return GdUnitResult.success(clazz.get_class())
|
|
|
|
# extract name form full qualified class path
|
|
if clazz is String:
|
|
if ClassDB.class_exists(clazz):
|
|
return GdUnitResult.success(clazz)
|
|
var source_sript :Script = load(clazz)
|
|
var clazz_name :String = load("res://addons/gdUnit4/src/core/parse/GdScriptParser.gd").new().get_class_name(source_sript)
|
|
return GdUnitResult.success(to_pascal_case(clazz_name))
|
|
|
|
if is_primitive_type(clazz):
|
|
return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz)))
|
|
|
|
if is_script(clazz):
|
|
if clazz.resource_path.is_empty():
|
|
var class_path := extract_class_name_from_class_path(extract_class_path(clazz))
|
|
return GdUnitResult.success(class_path);
|
|
return extract_class_name(clazz.resource_path)
|
|
|
|
# need to create an instance for a class typ the extract the class name
|
|
var instance :Variant = clazz.new()
|
|
if instance == null:
|
|
return GdUnitResult.error("Can't create a instance for class '%s'" % clazz)
|
|
var result := extract_class_name(instance)
|
|
GdUnitTools.free_instance(instance)
|
|
return result
|
|
|
|
|
|
static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStringArray) -> PackedStringArray:
|
|
var inner_classes := PackedStringArray()
|
|
|
|
if ClassDB.class_exists(clazz_name):
|
|
return inner_classes
|
|
var script :GDScript = load(script_path[0])
|
|
var map := script.get_script_constant_map()
|
|
for key in map.keys():
|
|
var value = map.get(key)
|
|
if value is GDScript:
|
|
var class_path := extract_class_path(value)
|
|
inner_classes.append(class_path[1])
|
|
return inner_classes
|
|
|
|
|
|
static func extract_class_functions(clazz_name :String, script_path :PackedStringArray) -> Array:
|
|
if ClassDB.class_get_method_list(clazz_name):
|
|
return ClassDB.class_get_method_list(clazz_name)
|
|
|
|
if not FileAccess.file_exists(script_path[0]):
|
|
return Array()
|
|
var script :GDScript = load(script_path[0])
|
|
if script is GDScript:
|
|
# if inner class on class path we have to load the script from the script_constant_map
|
|
if script_path.size() == 2 and script_path[1] != "":
|
|
var inner_classes := script_path[1]
|
|
var map := script.get_script_constant_map()
|
|
script = map[inner_classes]
|
|
var clazz_functions :Array = script.get_method_list()
|
|
var base_clazz :String = script.get_instance_base_type()
|
|
if base_clazz:
|
|
return extract_class_functions(base_clazz, script_path)
|
|
return clazz_functions
|
|
return Array()
|
|
|
|
|
|
# scans all registert script classes for given <clazz_name>
|
|
# if the class is public in the global space than return true otherwise false
|
|
# public class means the script class is defined by 'class_name <name>'
|
|
static func is_public_script_class(clazz_name :String) -> bool:
|
|
var script_classes:Array[Dictionary] = ProjectSettings.get_global_class_list()
|
|
for class_info in script_classes:
|
|
if class_info.has("class"):
|
|
if class_info["class"] == clazz_name:
|
|
return true
|
|
return false
|
|
|
|
|
|
static func build_function_default_arguments(script :GDScript, func_name :String) -> Array:
|
|
var arg_list := Array()
|
|
for func_sig in script.get_script_method_list():
|
|
if func_sig["name"] == func_name:
|
|
var args :Array = func_sig["args"]
|
|
for arg in args:
|
|
var value_type := arg["type"] as int
|
|
var default_value = default_value_by_type(value_type)
|
|
arg_list.append(default_value)
|
|
return arg_list
|
|
return arg_list
|
|
|
|
|
|
static func default_value_by_type(type :int):
|
|
assert(type < TYPE_MAX)
|
|
assert(type >= 0)
|
|
|
|
match type:
|
|
TYPE_NIL: return null
|
|
TYPE_BOOL: return false
|
|
TYPE_INT: return 0
|
|
TYPE_FLOAT: return 0.0
|
|
TYPE_STRING: return ""
|
|
TYPE_VECTOR2: return Vector2.ZERO
|
|
TYPE_VECTOR2I: return Vector2i.ZERO
|
|
TYPE_VECTOR3: return Vector3.ZERO
|
|
TYPE_VECTOR3I: return Vector3i.ZERO
|
|
TYPE_VECTOR4: return Vector4.ZERO
|
|
TYPE_VECTOR4I: return Vector4i.ZERO
|
|
TYPE_RECT2: return Rect2()
|
|
TYPE_RECT2I: return Rect2i()
|
|
TYPE_TRANSFORM2D: return Transform2D()
|
|
TYPE_PLANE: return Plane()
|
|
TYPE_QUATERNION: return Quaternion()
|
|
TYPE_AABB: return AABB()
|
|
TYPE_BASIS: return Basis()
|
|
TYPE_TRANSFORM3D: return Transform3D()
|
|
TYPE_COLOR: return Color()
|
|
TYPE_NODE_PATH: return NodePath()
|
|
TYPE_RID: return RID()
|
|
TYPE_OBJECT: return null
|
|
TYPE_ARRAY: return []
|
|
TYPE_DICTIONARY: return {}
|
|
TYPE_PACKED_BYTE_ARRAY: return PackedByteArray()
|
|
TYPE_PACKED_COLOR_ARRAY: return PackedColorArray()
|
|
TYPE_PACKED_INT32_ARRAY: return PackedInt32Array()
|
|
TYPE_PACKED_INT64_ARRAY: return PackedInt64Array()
|
|
TYPE_PACKED_FLOAT32_ARRAY: return PackedFloat32Array()
|
|
TYPE_PACKED_FLOAT64_ARRAY: return PackedFloat64Array()
|
|
TYPE_PACKED_STRING_ARRAY: return PackedStringArray()
|
|
TYPE_PACKED_VECTOR2_ARRAY: return PackedVector2Array()
|
|
TYPE_PACKED_VECTOR3_ARRAY: return PackedVector3Array()
|
|
|
|
push_error("Can't determine a default value for type: '%s', Please create a Bug issue and attach the stacktrace please." % type)
|
|
return null
|
|
|
|
|
|
static func find_nodes_by_class(root: Node, cls: String, recursive: bool = false) -> Array[Node]:
|
|
if not recursive:
|
|
return _find_nodes_by_class_no_rec(root, cls)
|
|
return _find_nodes_by_class(root, cls)
|
|
|
|
|
|
static func _find_nodes_by_class_no_rec(parent: Node, cls: String) -> Array[Node]:
|
|
var result :Array[Node] = []
|
|
for ch in parent.get_children():
|
|
if ch.get_class() == cls:
|
|
result.append(ch)
|
|
return result
|
|
|
|
|
|
static func _find_nodes_by_class(root: Node, cls: String) -> Array[Node]:
|
|
var result :Array[Node] = []
|
|
var stack :Array[Node] = [root]
|
|
while stack:
|
|
var node :Node = stack.pop_back()
|
|
if node.get_class() == cls:
|
|
result.append(node)
|
|
for ch in node.get_children():
|
|
stack.push_back(ch)
|
|
return result
|