trying to fix Export
All checks were successful
Create tag and build when new code gets to main / Export (push) Successful in 6m53s
All checks were successful
Create tag and build when new code gets to main / Export (push) Successful in 6m53s
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
class_name GdClassDescriptor
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var _name :String
|
||||
var _is_inner_class :bool
|
||||
var _functions :Array[GdFunctionDescriptor]
|
||||
|
||||
|
||||
func _init(p_name :String, p_is_inner_class :bool, p_functions :Array[GdFunctionDescriptor]) -> void:
|
||||
_name = p_name
|
||||
_is_inner_class = p_is_inner_class
|
||||
_functions = p_functions
|
||||
|
||||
|
||||
func name() -> String:
|
||||
return _name
|
||||
|
||||
|
||||
func is_inner_class() -> bool:
|
||||
return _is_inner_class
|
||||
|
||||
|
||||
func functions() -> Array[GdFunctionDescriptor]:
|
||||
return _functions
|
||||
@@ -1 +0,0 @@
|
||||
uid://c1ipsxino6xxt
|
||||
@@ -1,290 +0,0 @@
|
||||
# holds all decodings for default values
|
||||
class_name GdDefaultValueDecoder
|
||||
extends GdUnitSingleton
|
||||
|
||||
|
||||
@warning_ignore("unused_parameter")
|
||||
var _decoders := {
|
||||
TYPE_NIL: func(value :Variant) -> String: return "null",
|
||||
TYPE_STRING: func(value :Variant) -> String: return '"%s"' % value,
|
||||
TYPE_STRING_NAME: _on_type_StringName,
|
||||
TYPE_BOOL: func(value :Variant) -> String: return str(value).to_lower(),
|
||||
TYPE_FLOAT: func(value :Variant) -> String: return '%f' % value,
|
||||
TYPE_COLOR: _on_type_Color,
|
||||
TYPE_ARRAY: _on_type_Array.bind(TYPE_ARRAY),
|
||||
TYPE_PACKED_BYTE_ARRAY: _on_type_Array.bind(TYPE_PACKED_BYTE_ARRAY),
|
||||
TYPE_PACKED_STRING_ARRAY: _on_type_Array.bind(TYPE_PACKED_STRING_ARRAY),
|
||||
TYPE_PACKED_FLOAT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT32_ARRAY),
|
||||
TYPE_PACKED_FLOAT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT64_ARRAY),
|
||||
TYPE_PACKED_INT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT32_ARRAY),
|
||||
TYPE_PACKED_INT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT64_ARRAY),
|
||||
TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY),
|
||||
TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY),
|
||||
TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY),
|
||||
TYPE_PACKED_VECTOR4_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR4_ARRAY),
|
||||
TYPE_DICTIONARY: _on_type_Dictionary,
|
||||
TYPE_RID: _on_type_RID,
|
||||
TYPE_NODE_PATH: _on_type_NodePath,
|
||||
TYPE_VECTOR2: _on_type_Vector.bind(TYPE_VECTOR2),
|
||||
TYPE_VECTOR2I: _on_type_Vector.bind(TYPE_VECTOR2I),
|
||||
TYPE_VECTOR3: _on_type_Vector.bind(TYPE_VECTOR3),
|
||||
TYPE_VECTOR3I: _on_type_Vector.bind(TYPE_VECTOR3I),
|
||||
TYPE_VECTOR4: _on_type_Vector.bind(TYPE_VECTOR4),
|
||||
TYPE_VECTOR4I: _on_type_Vector.bind(TYPE_VECTOR4I),
|
||||
TYPE_RECT2: _on_type_Rect2,
|
||||
TYPE_RECT2I: _on_type_Rect2i,
|
||||
TYPE_PLANE: _on_type_Plane,
|
||||
TYPE_QUATERNION: _on_type_Quaternion,
|
||||
TYPE_AABB: _on_type_AABB,
|
||||
TYPE_BASIS: _on_type_Basis,
|
||||
TYPE_CALLABLE: _on_type_Callable,
|
||||
TYPE_SIGNAL: _on_type_Signal,
|
||||
TYPE_TRANSFORM2D: _on_type_Transform2D,
|
||||
TYPE_TRANSFORM3D: _on_type_Transform3D,
|
||||
TYPE_PROJECTION: _on_type_Projection,
|
||||
TYPE_OBJECT: _on_type_Object
|
||||
}
|
||||
|
||||
static func _regex(pattern: String) -> RegEx:
|
||||
var regex := RegEx.new()
|
||||
var err := regex.compile(pattern)
|
||||
if err != OK:
|
||||
push_error("error '%s' checked pattern '%s'" % [err, pattern])
|
||||
return null
|
||||
return regex
|
||||
|
||||
|
||||
func get_decoder(type: int) -> Callable:
|
||||
return _decoders.get(type, func(value :Variant) -> String: return '%s' % value)
|
||||
|
||||
|
||||
func _on_type_StringName(value: StringName) -> String:
|
||||
if value.is_empty():
|
||||
return 'StringName()'
|
||||
return 'StringName("%s")' % value
|
||||
|
||||
|
||||
func _on_type_Object(value: Variant, _type: int) -> String:
|
||||
return str(value)
|
||||
|
||||
|
||||
func _on_type_Color(color: Color) -> String:
|
||||
if color == Color.BLACK:
|
||||
return "Color()"
|
||||
return "Color%s" % color
|
||||
|
||||
|
||||
func _on_type_NodePath(path: NodePath) -> String:
|
||||
if path.is_empty():
|
||||
return 'NodePath()'
|
||||
return 'NodePath("%s")' % path
|
||||
|
||||
|
||||
func _on_type_Callable(_cb: Callable) -> String:
|
||||
return 'Callable()'
|
||||
|
||||
|
||||
func _on_type_Signal(_s: Signal) -> String:
|
||||
return 'Signal()'
|
||||
|
||||
|
||||
func _on_type_Dictionary(dict: Dictionary) -> String:
|
||||
if dict.is_empty():
|
||||
return '{}'
|
||||
return str(dict)
|
||||
|
||||
|
||||
func _on_type_Array(value: Variant, type: int) -> String:
|
||||
match type:
|
||||
TYPE_ARRAY:
|
||||
return str(value)
|
||||
|
||||
TYPE_PACKED_COLOR_ARRAY:
|
||||
var colors := PackedStringArray()
|
||||
for color: Color in value:
|
||||
@warning_ignore("return_value_discarded")
|
||||
colors.append(_on_type_Color(color))
|
||||
if colors.is_empty():
|
||||
return "PackedColorArray()"
|
||||
return "PackedColorArray([%s])" % ", ".join(colors)
|
||||
|
||||
TYPE_PACKED_VECTOR2_ARRAY:
|
||||
var vectors := PackedStringArray()
|
||||
for vector: Vector2 in value:
|
||||
@warning_ignore("return_value_discarded")
|
||||
vectors.append(_on_type_Vector(vector, TYPE_VECTOR2))
|
||||
if vectors.is_empty():
|
||||
return "PackedVector2Array()"
|
||||
return "PackedVector2Array([%s])" % ", ".join(vectors)
|
||||
|
||||
TYPE_PACKED_VECTOR3_ARRAY:
|
||||
var vectors := PackedStringArray()
|
||||
for vector: Vector3 in value:
|
||||
@warning_ignore("return_value_discarded")
|
||||
vectors.append(_on_type_Vector(vector, TYPE_VECTOR3))
|
||||
if vectors.is_empty():
|
||||
return "PackedVector3Array()"
|
||||
return "PackedVector3Array([%s])" % ", ".join(vectors)
|
||||
|
||||
TYPE_PACKED_VECTOR4_ARRAY:
|
||||
var vectors := PackedStringArray()
|
||||
for vector: Vector4 in value:
|
||||
@warning_ignore("return_value_discarded")
|
||||
vectors.append(_on_type_Vector(vector, TYPE_VECTOR4))
|
||||
if vectors.is_empty():
|
||||
return "PackedVector4Array()"
|
||||
return "PackedVector4Array([%s])" % ", ".join(vectors)
|
||||
|
||||
TYPE_PACKED_STRING_ARRAY:
|
||||
var values := PackedStringArray()
|
||||
for v: String in value:
|
||||
@warning_ignore("return_value_discarded")
|
||||
values.append('"%s"' % v)
|
||||
if values.is_empty():
|
||||
return "PackedStringArray()"
|
||||
return "PackedStringArray([%s])" % ", ".join(values)
|
||||
|
||||
TYPE_PACKED_BYTE_ARRAY,\
|
||||
TYPE_PACKED_FLOAT32_ARRAY,\
|
||||
TYPE_PACKED_FLOAT64_ARRAY,\
|
||||
TYPE_PACKED_INT32_ARRAY,\
|
||||
TYPE_PACKED_INT64_ARRAY:
|
||||
var vectors := PackedStringArray()
|
||||
for vector: Variant in value:
|
||||
@warning_ignore("return_value_discarded")
|
||||
vectors.append(str(vector))
|
||||
if vectors.is_empty():
|
||||
return GdObjects.type_as_string(type) + "()"
|
||||
return "%s([%s])" % [GdObjects.type_as_string(type), ", ".join(vectors)]
|
||||
return "unknown array type %d" % type
|
||||
|
||||
|
||||
func _on_type_Vector(value: Variant, type: int) -> String:
|
||||
|
||||
if typeof(value) != type:
|
||||
push_error("Internal Error: type missmatch detected for value '%s', expects type %s" % [value, type_string(type)])
|
||||
return ""
|
||||
|
||||
match type:
|
||||
TYPE_VECTOR2:
|
||||
if value == Vector2():
|
||||
return "Vector2()"
|
||||
return "Vector2%s" % value
|
||||
TYPE_VECTOR2I:
|
||||
if value == Vector2i():
|
||||
return "Vector2i()"
|
||||
return "Vector2i%s" % value
|
||||
TYPE_VECTOR3:
|
||||
if value == Vector3():
|
||||
return "Vector3()"
|
||||
return "Vector3%s" % value
|
||||
TYPE_VECTOR3I:
|
||||
if value == Vector3i():
|
||||
return "Vector3i()"
|
||||
return "Vector3i%s" % value
|
||||
TYPE_VECTOR4:
|
||||
if value == Vector4():
|
||||
return "Vector4()"
|
||||
return "Vector4%s" % value
|
||||
TYPE_VECTOR4I:
|
||||
if value == Vector4i():
|
||||
return "Vector4i()"
|
||||
return "Vector4i%s" % value
|
||||
return "unknown vector type %d" % type
|
||||
|
||||
|
||||
func _on_type_Transform2D(transform: Transform2D) -> String:
|
||||
if transform == Transform2D():
|
||||
return "Transform2D()"
|
||||
return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin]
|
||||
|
||||
|
||||
func _on_type_Transform3D(transform: Transform3D) -> String:
|
||||
if transform == Transform3D():
|
||||
return "Transform3D()"
|
||||
return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin]
|
||||
|
||||
|
||||
func _on_type_Projection(projection: Projection) -> String:
|
||||
return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w]
|
||||
|
||||
|
||||
@warning_ignore("unused_parameter")
|
||||
func _on_type_RID(value: RID) -> String:
|
||||
return "RID()"
|
||||
|
||||
|
||||
func _on_type_Rect2(rect: Rect2) -> String:
|
||||
if rect == Rect2():
|
||||
return "Rect2()"
|
||||
return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size]
|
||||
|
||||
|
||||
func _on_type_Rect2i(rect: Variant) -> String:
|
||||
if rect == Rect2i():
|
||||
return "Rect2i()"
|
||||
return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size]
|
||||
|
||||
|
||||
func _on_type_Plane(plane: Plane) -> String:
|
||||
if plane == Plane():
|
||||
return "Plane()"
|
||||
return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d]
|
||||
|
||||
|
||||
func _on_type_Quaternion(quaternion: Quaternion) -> String:
|
||||
if quaternion == Quaternion():
|
||||
return "Quaternion()"
|
||||
return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w]
|
||||
|
||||
|
||||
func _on_type_AABB(aabb: AABB) -> String:
|
||||
if aabb == AABB():
|
||||
return "AABB()"
|
||||
return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size]
|
||||
|
||||
|
||||
func _on_type_Basis(basis: Basis) -> String:
|
||||
if basis == Basis():
|
||||
return "Basis()"
|
||||
return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z]
|
||||
|
||||
|
||||
static func decode(value: Variant) -> String:
|
||||
var type := typeof(value)
|
||||
@warning_ignore("unsafe_cast")
|
||||
if GdArrayTools.is_type_array(type) and (value as Array).is_empty():
|
||||
return "<empty>"
|
||||
# For Variant types we need to determine the original type
|
||||
if type == GdObjects.TYPE_VARIANT:
|
||||
type = typeof(value)
|
||||
var decoder := _get_value_decoder(type)
|
||||
if decoder == null:
|
||||
push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type)
|
||||
return "null"
|
||||
if type == TYPE_OBJECT:
|
||||
return decoder.call(value, type)
|
||||
return decoder.call(value)
|
||||
|
||||
|
||||
static func decode_typed(type: int, value: Variant) -> String:
|
||||
if value == null:
|
||||
return "null"
|
||||
# For Variant types we need to determine the original type
|
||||
if type == GdObjects.TYPE_VARIANT:
|
||||
type = typeof(value)
|
||||
var decoder := _get_value_decoder(type)
|
||||
if decoder == null:
|
||||
push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type)
|
||||
return "null"
|
||||
if type == TYPE_OBJECT:
|
||||
return decoder.call(value, type)
|
||||
return decoder.call(value)
|
||||
|
||||
|
||||
static func _get_value_decoder(type: int) -> Callable:
|
||||
var decoder: GdDefaultValueDecoder = instance(
|
||||
"GdUnitDefaultValueDecoders",
|
||||
func() -> GdDefaultValueDecoder:
|
||||
return GdDefaultValueDecoder.new())
|
||||
return decoder.get_decoder(type)
|
||||
@@ -1 +0,0 @@
|
||||
uid://lklwx7a3htjd
|
||||
@@ -1,208 +0,0 @@
|
||||
class_name GdFunctionArgument
|
||||
extends RefCounted
|
||||
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
const UNDEFINED: String = "<-NO_ARG->"
|
||||
const ARG_PARAMETERIZED_TEST := ["test_parameters", "_test_parameters"]
|
||||
|
||||
static var _fuzzer_regex: RegEx
|
||||
static var _cleanup_leading_spaces: RegEx
|
||||
static var _fix_comma_space: RegEx
|
||||
|
||||
var _name: String
|
||||
var _type: int
|
||||
var _type_hint: int
|
||||
var _default_value: Variant
|
||||
var _parameter_sets: PackedStringArray = []
|
||||
|
||||
|
||||
func _init(p_name: String, p_type: int, value: Variant = UNDEFINED, p_type_hint: int = TYPE_NIL) -> void:
|
||||
_init_static_variables()
|
||||
_name = p_name
|
||||
_type = p_type
|
||||
_type_hint = p_type_hint
|
||||
if value != null and p_name in ARG_PARAMETERIZED_TEST:
|
||||
_parameter_sets = _parse_parameter_set(str(value))
|
||||
_default_value = value
|
||||
# is argument a fuzzer?
|
||||
if _type == TYPE_OBJECT and _fuzzer_regex.search(_name):
|
||||
_type = GdObjects.TYPE_FUZZER
|
||||
|
||||
|
||||
func _init_static_variables() -> void:
|
||||
if _fuzzer_regex == null:
|
||||
_fuzzer_regex = GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)")
|
||||
_cleanup_leading_spaces = RegEx.create_from_string("(?m)^[ \t]+")
|
||||
_fix_comma_space = RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""")
|
||||
|
||||
|
||||
func name() -> String:
|
||||
return _name
|
||||
|
||||
|
||||
func default() -> Variant:
|
||||
return type_convert(_default_value, _type)
|
||||
|
||||
|
||||
func set_value(value: String) -> void:
|
||||
# we onle need to apply default values for Objects, all others are provided by the method descriptor
|
||||
if _type == GdObjects.TYPE_FUZZER:
|
||||
_default_value = value
|
||||
return
|
||||
if _name in ARG_PARAMETERIZED_TEST:
|
||||
_parameter_sets = _parse_parameter_set(value)
|
||||
_default_value = value
|
||||
return
|
||||
|
||||
if _type == TYPE_NIL or _type == GdObjects.TYPE_VARIANT:
|
||||
_type = _extract_value_type(value)
|
||||
if _type == GdObjects.TYPE_VARIANT and _default_value == null:
|
||||
_default_value = value
|
||||
if _default_value == null:
|
||||
match _type:
|
||||
TYPE_DICTIONARY:
|
||||
_default_value = as_dictionary(value)
|
||||
TYPE_ARRAY:
|
||||
_default_value = as_array(value)
|
||||
GdObjects.TYPE_FUZZER:
|
||||
_default_value = value
|
||||
_:
|
||||
_default_value = str_to_var(value)
|
||||
# if converting fails assign the original value without converting
|
||||
if _default_value == null and value != null:
|
||||
_default_value = value
|
||||
#prints("set default_value: ", _default_value, "with type %d" % _type, " from original: '%s'" % value)
|
||||
|
||||
|
||||
func _extract_value_type(value: String) -> int:
|
||||
if value != UNDEFINED:
|
||||
if _fuzzer_regex.search(_name):
|
||||
return GdObjects.TYPE_FUZZER
|
||||
if value.rfind(")") == value.length()-1:
|
||||
return GdObjects.TYPE_FUNC
|
||||
return _type
|
||||
|
||||
|
||||
func value_as_string() -> String:
|
||||
if has_default():
|
||||
return GdDefaultValueDecoder.decode_typed(_type, _default_value)
|
||||
return ""
|
||||
|
||||
|
||||
func plain_value() -> Variant:
|
||||
return _default_value
|
||||
|
||||
|
||||
func type() -> int:
|
||||
return _type
|
||||
|
||||
|
||||
func type_hint() -> int:
|
||||
return _type_hint
|
||||
|
||||
|
||||
func has_default() -> bool:
|
||||
return not is_same(_default_value, UNDEFINED)
|
||||
|
||||
|
||||
func is_typed_array() -> bool:
|
||||
return _type == TYPE_ARRAY and _type_hint != TYPE_NIL
|
||||
|
||||
|
||||
func is_parameter_set() -> bool:
|
||||
return _name in ARG_PARAMETERIZED_TEST
|
||||
|
||||
|
||||
func parameter_sets() -> PackedStringArray:
|
||||
return _parameter_sets
|
||||
|
||||
|
||||
static func get_parameter_set(parameters :Array[GdFunctionArgument]) -> GdFunctionArgument:
|
||||
for current in parameters:
|
||||
if current != null and current.is_parameter_set():
|
||||
return current
|
||||
return null
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var s := _name
|
||||
if _type != TYPE_NIL:
|
||||
s += ": " + GdObjects.type_as_string(_type)
|
||||
if _type_hint != TYPE_NIL:
|
||||
s += "[%s]" % GdObjects.type_as_string(_type_hint)
|
||||
if has_default():
|
||||
s += "=" + value_as_string()
|
||||
return s
|
||||
|
||||
|
||||
func _parse_parameter_set(input :String) -> PackedStringArray:
|
||||
if not input.contains("["):
|
||||
return []
|
||||
|
||||
input = _cleanup_leading_spaces.sub(input, "", true)
|
||||
input = input.replace("\n", "").strip_edges().trim_prefix("[").trim_suffix("]").trim_prefix("]")
|
||||
var single_quote := false
|
||||
var double_quote := false
|
||||
var array_end := 0
|
||||
var current_index := 0
|
||||
var output :PackedStringArray = []
|
||||
var buf := input.to_utf8_buffer()
|
||||
var collected_characters: = PackedByteArray()
|
||||
var matched :bool = false
|
||||
|
||||
for c in buf:
|
||||
current_index += 1
|
||||
matched = current_index == buf.size()
|
||||
@warning_ignore("return_value_discarded")
|
||||
collected_characters.push_back(c)
|
||||
|
||||
match c:
|
||||
# ' ': ignore spaces between array elements
|
||||
32: if array_end == 0 and (not double_quote and not single_quote):
|
||||
collected_characters.remove_at(collected_characters.size()-1)
|
||||
# ',': step over array element seperator ','
|
||||
44: if array_end == 0:
|
||||
matched = true
|
||||
collected_characters.remove_at(collected_characters.size()-1)
|
||||
# '`':
|
||||
39: single_quote = !single_quote
|
||||
# '"':
|
||||
34: if not single_quote: double_quote = !double_quote
|
||||
# '['
|
||||
91: if not double_quote and not single_quote: array_end +=1 # counts array open
|
||||
# ']'
|
||||
93: if not double_quote and not single_quote: array_end -=1 # counts array closed
|
||||
|
||||
# if array closed than collect the element
|
||||
if matched:
|
||||
var parameters := _fix_comma_space.sub(collected_characters.get_string_from_utf8(), ", ", true)
|
||||
if not parameters.is_empty():
|
||||
@warning_ignore("return_value_discarded")
|
||||
output.append(parameters)
|
||||
collected_characters.clear()
|
||||
matched = false
|
||||
return output
|
||||
|
||||
|
||||
## value converters
|
||||
|
||||
func as_array(value: String) -> Array:
|
||||
if value == "Array()" or value == "[]":
|
||||
return []
|
||||
|
||||
if value.begins_with("Array("):
|
||||
value = value.lstrip("Array(").rstrip(")")
|
||||
if value.begins_with("["):
|
||||
return str_to_var(value)
|
||||
return []
|
||||
|
||||
|
||||
func as_dictionary(value: String) -> Dictionary:
|
||||
if value == "Dictionary()":
|
||||
return {}
|
||||
if value.begins_with("Dictionary("):
|
||||
value = value.lstrip("Dictionary(").rstrip(")")
|
||||
if value.begins_with("{"):
|
||||
return str_to_var(value)
|
||||
return {}
|
||||
@@ -1 +0,0 @@
|
||||
uid://c1fyr61upo4ts
|
||||
@@ -1,286 +0,0 @@
|
||||
class_name GdFunctionDescriptor
|
||||
extends RefCounted
|
||||
|
||||
var _is_virtual :bool
|
||||
var _is_static :bool
|
||||
var _is_engine :bool
|
||||
var _is_coroutine :bool
|
||||
var _name :String
|
||||
var _source_path: String
|
||||
var _line_number :int
|
||||
var _return_type :int
|
||||
var _return_class :String
|
||||
var _args : Array[GdFunctionArgument]
|
||||
var _varargs :Array[GdFunctionArgument]
|
||||
|
||||
|
||||
|
||||
static func create(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor:
|
||||
var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, false, false, p_return_type, "", p_args)
|
||||
fd.enrich_file_info(p_source_path, p_source_line)
|
||||
return fd
|
||||
|
||||
static func create_static(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor:
|
||||
var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, true, false, p_return_type, "", p_args)
|
||||
fd.enrich_file_info(p_source_path, p_source_line)
|
||||
return fd
|
||||
|
||||
|
||||
func _init(p_name :String,
|
||||
p_line_number :int,
|
||||
p_is_virtual :bool,
|
||||
p_is_static :bool,
|
||||
p_is_engine :bool,
|
||||
p_return_type :int,
|
||||
p_return_class :String,
|
||||
p_args : Array[GdFunctionArgument],
|
||||
p_varargs :Array[GdFunctionArgument] = []) -> void:
|
||||
_name = p_name
|
||||
_line_number = p_line_number
|
||||
_return_type = p_return_type
|
||||
_return_class = p_return_class
|
||||
_is_virtual = p_is_virtual
|
||||
_is_static = p_is_static
|
||||
_is_engine = p_is_engine
|
||||
_is_coroutine = false
|
||||
_args = p_args
|
||||
_varargs = p_varargs
|
||||
|
||||
|
||||
func with_return_class(clazz_name: String) -> GdFunctionDescriptor:
|
||||
_return_class = clazz_name
|
||||
return self
|
||||
|
||||
|
||||
func name() -> String:
|
||||
return _name
|
||||
|
||||
|
||||
func source_path() -> String:
|
||||
return _source_path
|
||||
|
||||
|
||||
func line_number() -> int:
|
||||
return _line_number
|
||||
|
||||
|
||||
func is_virtual() -> bool:
|
||||
return _is_virtual
|
||||
|
||||
|
||||
func is_static() -> bool:
|
||||
return _is_static
|
||||
|
||||
|
||||
func is_engine() -> bool:
|
||||
return _is_engine
|
||||
|
||||
|
||||
func is_vararg() -> bool:
|
||||
return not _varargs.is_empty()
|
||||
|
||||
|
||||
func is_coroutine() -> bool:
|
||||
return _is_coroutine
|
||||
|
||||
|
||||
func is_parameterized() -> bool:
|
||||
for current in _args:
|
||||
var arg :GdFunctionArgument = current
|
||||
if arg.name() in GdFunctionArgument.ARG_PARAMETERIZED_TEST:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func is_private() -> bool:
|
||||
return name().begins_with("_") and not is_virtual()
|
||||
|
||||
|
||||
func return_type() -> int:
|
||||
return _return_type
|
||||
|
||||
|
||||
func return_type_as_string() -> String:
|
||||
if return_type() == TYPE_NIL:
|
||||
return "void"
|
||||
if (return_type() == TYPE_OBJECT or return_type() == GdObjects.TYPE_ENUM) and not _return_class.is_empty():
|
||||
return _return_class
|
||||
return GdObjects.type_as_string(return_type())
|
||||
|
||||
|
||||
func set_argument_value(arg_name: String, value: String) -> void:
|
||||
var argument: GdFunctionArgument = _args.filter(func(arg: GdFunctionArgument) -> bool:
|
||||
return arg.name() == arg_name
|
||||
).front()
|
||||
if argument != null:
|
||||
argument.set_value(value)
|
||||
|
||||
|
||||
func enrich_arguments(arguments: Array[Dictionary]) -> void:
|
||||
for arg_index: int in arguments.size():
|
||||
var arg: Dictionary = arguments[arg_index]
|
||||
if arg["type"] != GdObjects.TYPE_VARARG:
|
||||
var arg_name: String = arg["name"]
|
||||
var arg_value: String = arg["value"]
|
||||
set_argument_value(arg_name, arg_value)
|
||||
|
||||
|
||||
func enrich_file_info(p_source_path: String, p_line_number: int) -> void:
|
||||
_source_path = p_source_path
|
||||
_line_number = p_line_number
|
||||
|
||||
|
||||
func args() -> Array[GdFunctionArgument]:
|
||||
return _args
|
||||
|
||||
|
||||
func varargs() -> Array[GdFunctionArgument]:
|
||||
return _varargs
|
||||
|
||||
|
||||
func typed_args() -> String:
|
||||
var collect := PackedStringArray()
|
||||
for arg in args():
|
||||
@warning_ignore("return_value_discarded")
|
||||
collect.push_back(arg._to_string())
|
||||
for arg in varargs():
|
||||
@warning_ignore("return_value_discarded")
|
||||
collect.push_back(arg._to_string())
|
||||
return ", ".join(collect)
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
var fsignature := "virtual " if is_virtual() else ""
|
||||
if _return_type == TYPE_NIL:
|
||||
return fsignature + "[Line:%s] func %s(%s):" % [line_number(), name(), typed_args()]
|
||||
var func_template := fsignature + "[Line:%s] func %s(%s) -> %s:"
|
||||
if is_static():
|
||||
func_template= "[Line:%s] static func %s(%s) -> %s:"
|
||||
return func_template % [line_number(), name(), typed_args(), return_type_as_string()]
|
||||
|
||||
|
||||
# extract function description given by Object.get_method_list()
|
||||
static func extract_from(descriptor :Dictionary, is_engine_ := true) -> GdFunctionDescriptor:
|
||||
var func_name: String = descriptor["name"]
|
||||
var function_flags: int = descriptor["flags"]
|
||||
var return_descriptor: Dictionary = descriptor["return"]
|
||||
var clazz_name: String = return_descriptor["class_name"]
|
||||
var is_virtual_: bool = function_flags & METHOD_FLAG_VIRTUAL
|
||||
var is_static_: bool = function_flags & METHOD_FLAG_STATIC
|
||||
var is_vararg_: bool = function_flags & METHOD_FLAG_VARARG
|
||||
|
||||
return GdFunctionDescriptor.new(
|
||||
func_name,
|
||||
-1,
|
||||
is_virtual_,
|
||||
is_static_,
|
||||
is_engine_,
|
||||
_extract_return_type(return_descriptor),
|
||||
clazz_name,
|
||||
_extract_args(descriptor),
|
||||
_build_varargs(is_vararg_)
|
||||
)
|
||||
|
||||
# temporary exclude GlobalScope enums
|
||||
const enum_fix := [
|
||||
"Side",
|
||||
"Corner",
|
||||
"Orientation",
|
||||
"ClockDirection",
|
||||
"HorizontalAlignment",
|
||||
"VerticalAlignment",
|
||||
"InlineAlignment",
|
||||
"EulerOrder",
|
||||
"Error",
|
||||
"Key",
|
||||
"MIDIMessage",
|
||||
"MouseButton",
|
||||
"MouseButtonMask",
|
||||
"JoyButton",
|
||||
"JoyAxis",
|
||||
"PropertyHint",
|
||||
"PropertyUsageFlags",
|
||||
"MethodFlags",
|
||||
"Variant.Type",
|
||||
"Control.LayoutMode"]
|
||||
|
||||
|
||||
static func _extract_return_type(return_info :Dictionary) -> int:
|
||||
var type :int = return_info["type"]
|
||||
var usage :int = return_info["usage"]
|
||||
if type == TYPE_INT and usage & PROPERTY_USAGE_CLASS_IS_ENUM:
|
||||
return GdObjects.TYPE_ENUM
|
||||
if type == TYPE_NIL and usage & PROPERTY_USAGE_NIL_IS_VARIANT:
|
||||
return GdObjects.TYPE_VARIANT
|
||||
if type == TYPE_NIL and usage == 6:
|
||||
return GdObjects.TYPE_VOID
|
||||
return type
|
||||
|
||||
|
||||
static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]:
|
||||
var args_ :Array[GdFunctionArgument] = []
|
||||
var arguments :Array = descriptor["args"]
|
||||
var defaults :Array = descriptor["default_args"]
|
||||
# iterate backwards because the default values are stored from right to left
|
||||
while not arguments.is_empty():
|
||||
var arg :Dictionary = arguments.pop_back()
|
||||
var arg_name := _argument_name(arg)
|
||||
var arg_type := _argument_type(arg)
|
||||
var arg_type_hint := _argument_hint(arg)
|
||||
#var arg_class: StringName = arg["class_name"]
|
||||
var default_value: Variant = GdFunctionArgument.UNDEFINED if defaults.is_empty() else defaults.pop_back()
|
||||
args_.push_front(GdFunctionArgument.new(arg_name, arg_type, default_value, arg_type_hint))
|
||||
return args_
|
||||
|
||||
|
||||
static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]:
|
||||
var varargs_ :Array[GdFunctionArgument] = []
|
||||
if not p_is_vararg:
|
||||
return varargs_
|
||||
varargs_.push_back(GdFunctionArgument.new("varargs", GdObjects.TYPE_VARARG, ''))
|
||||
return varargs_
|
||||
|
||||
|
||||
static func _argument_name(arg :Dictionary) -> String:
|
||||
return arg["name"]
|
||||
|
||||
|
||||
static func _argument_type(arg :Dictionary) -> int:
|
||||
var type :int = arg["type"]
|
||||
var usage :int = arg["usage"]
|
||||
|
||||
if type == TYPE_OBJECT:
|
||||
if arg["class_name"] == "Node":
|
||||
return GdObjects.TYPE_NODE
|
||||
if arg["class_name"] == "Fuzzer":
|
||||
return GdObjects.TYPE_FUZZER
|
||||
|
||||
# if the argument untyped we need to scan the assignef value type
|
||||
if type == TYPE_NIL and usage == PROPERTY_USAGE_NIL_IS_VARIANT:
|
||||
return GdObjects.TYPE_VARIANT
|
||||
return type
|
||||
|
||||
|
||||
static func _argument_hint(arg :Dictionary) -> int:
|
||||
var hint :int = arg["hint"]
|
||||
var hint_string :String = arg["hint_string"]
|
||||
|
||||
match hint:
|
||||
PROPERTY_HINT_ARRAY_TYPE:
|
||||
return GdObjects.string_to_type(hint_string)
|
||||
_:
|
||||
return 0
|
||||
|
||||
|
||||
static func _argument_type_as_string(arg :Dictionary) -> String:
|
||||
var type := _argument_type(arg)
|
||||
match type:
|
||||
TYPE_NIL:
|
||||
return ""
|
||||
TYPE_OBJECT:
|
||||
var clazz_name :String = arg["class_name"]
|
||||
if not clazz_name.is_empty():
|
||||
return clazz_name
|
||||
return ""
|
||||
_:
|
||||
return GdObjects.type_as_string(type)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bascqhwocxsl4
|
||||
@@ -1,188 +0,0 @@
|
||||
class_name GdFunctionParameterSetResolver
|
||||
extends RefCounted
|
||||
|
||||
const CLASS_TEMPLATE = """
|
||||
class_name _ParameterExtractor extends '${clazz_path}'
|
||||
|
||||
func __extract_test_parameters() -> Array:
|
||||
return ${test_params}
|
||||
|
||||
"""
|
||||
|
||||
const EXCLUDE_PROPERTIES_TO_COPY = [
|
||||
"script",
|
||||
"type",
|
||||
"Node",
|
||||
"_import_path"]
|
||||
|
||||
|
||||
var _fd: GdFunctionDescriptor
|
||||
var _static_sets_by_index := {}
|
||||
var _is_static := true
|
||||
|
||||
func _init(fd: GdFunctionDescriptor) -> void:
|
||||
_fd = fd
|
||||
|
||||
|
||||
func resolve_test_cases(script: GDScript) -> Array[GdUnitTestCase]:
|
||||
if not is_parameterized():
|
||||
return [GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name())]
|
||||
return extract_test_cases_by_reflection(script)
|
||||
|
||||
|
||||
func is_parameterized() -> bool:
|
||||
return _fd.is_parameterized()
|
||||
|
||||
|
||||
func is_parameter_sets_static() -> bool:
|
||||
return _is_static
|
||||
|
||||
|
||||
func is_parameter_set_static(index: int) -> bool:
|
||||
return _is_static and _static_sets_by_index.get(index, false)
|
||||
|
||||
|
||||
# validates the given arguments are complete and matches to required input fields of the test function
|
||||
func validate(input_value_set: Array) -> String:
|
||||
var input_arguments := _fd.args()
|
||||
# check given parameter set with test case arguments
|
||||
var expected_arg_count := input_arguments.size() - 1
|
||||
for input_values :Variant in input_value_set:
|
||||
var parameter_set_index := input_value_set.find(input_values)
|
||||
if input_values is Array:
|
||||
var arr_values: Array = input_values
|
||||
var current_arg_count := arr_values.size()
|
||||
if current_arg_count != expected_arg_count:
|
||||
return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count]
|
||||
var error := validate_parameter_types(input_arguments, arr_values, parameter_set_index)
|
||||
if not error.is_empty():
|
||||
return error
|
||||
else:
|
||||
return "\n The parameter set at index [%d] does not match the expected input parameters!\n Expecting an array of input values." % parameter_set_index
|
||||
return ""
|
||||
|
||||
|
||||
static func validate_parameter_types(input_arguments: Array, input_values: Array, parameter_set_index: int) -> String:
|
||||
for i in input_arguments.size():
|
||||
var input_param: GdFunctionArgument = input_arguments[i]
|
||||
# only check the test input arguments
|
||||
if input_param.is_parameter_set():
|
||||
continue
|
||||
var input_param_type := input_param.type()
|
||||
var input_value :Variant = input_values[i]
|
||||
var input_value_type := typeof(input_value)
|
||||
# input parameter is not typed or is Variant we skip the type test
|
||||
if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT:
|
||||
continue
|
||||
# is input type enum allow int values
|
||||
if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT:
|
||||
continue
|
||||
# allow only equal types and object == null
|
||||
if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL:
|
||||
continue
|
||||
if input_param_type != input_value_type:
|
||||
return "\n The parameter set at index [%d] does not match the expected input parameters!\n The value '%s' does not match the required input parameter <%s>." % [parameter_set_index, input_value, input_param]
|
||||
return ""
|
||||
|
||||
|
||||
func extract_test_cases_by_reflection(script: GDScript) -> Array[GdUnitTestCase]:
|
||||
var source: Node = script.new()
|
||||
source.queue_free()
|
||||
|
||||
var fa := GdFunctionArgument.get_parameter_set(_fd.args())
|
||||
var parameter_sets := fa.parameter_sets()
|
||||
# if no parameter set detected we need to resolve it by using reflection
|
||||
if parameter_sets.size() == 0:
|
||||
_is_static = false
|
||||
return _extract_test_cases_by_reflection(source, script)
|
||||
else:
|
||||
var test_cases: Array[GdUnitTestCase] = []
|
||||
var property_names := _extract_property_names(source)
|
||||
for parameter_set_index in parameter_sets.size():
|
||||
var parameter_set := parameter_sets[parameter_set_index]
|
||||
_static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names)
|
||||
@warning_ignore("return_value_discarded")
|
||||
test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), parameter_set_index, parameter_set))
|
||||
parameter_set_index += 1
|
||||
return test_cases
|
||||
|
||||
|
||||
func _extract_property_names(source: Node) -> PackedStringArray:
|
||||
return source.get_property_list()\
|
||||
.map(func(property :Dictionary) -> String: return property["name"])\
|
||||
.filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property))
|
||||
|
||||
|
||||
# tests if the test property set contains an property reference by name, if not the parameter set holds only static values
|
||||
func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool:
|
||||
for property_name in property_names:
|
||||
if parameters.contains(property_name):
|
||||
_is_static = false
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _extract_test_cases_by_reflection(source: Node, script: GDScript) -> Array[GdUnitTestCase]:
|
||||
var parameter_sets := load_parameter_sets(source)
|
||||
var test_cases: Array[GdUnitTestCase] = []
|
||||
for index in parameter_sets.size():
|
||||
var parameter_set := str(parameter_sets[index])
|
||||
@warning_ignore("return_value_discarded")
|
||||
test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), index, parameter_set))
|
||||
return test_cases
|
||||
|
||||
|
||||
# extracts the arguments from the given test case, using kind of reflection solution
|
||||
# to restore the parameters from a string representation to real instance type
|
||||
func load_parameter_sets(source: Node) -> Array:
|
||||
var source_script: GDScript = source.get_script()
|
||||
var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args())
|
||||
var source_code := CLASS_TEMPLATE \
|
||||
.replace("${clazz_path}", source_script.resource_path) \
|
||||
.replace("${test_params}", parameter_arg.value_as_string())
|
||||
var script := GDScript.new()
|
||||
script.source_code = source_code
|
||||
# enable this lines only for debuging
|
||||
#script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name()
|
||||
#DirAccess.remove_absolute(script.resource_path)
|
||||
#ResourceSaver.save(script, script.resource_path)
|
||||
var result := script.reload()
|
||||
if result != OK:
|
||||
push_error("Extracting test parameters failed! Script loading error: %s" % result)
|
||||
return []
|
||||
var instance: Node = script.new()
|
||||
GdFunctionParameterSetResolver.copy_properties(source, instance)
|
||||
instance.queue_free()
|
||||
var parameter_sets: Array = instance.call("__extract_test_parameters")
|
||||
return fixure_typed_parameters(parameter_sets, _fd.args())
|
||||
|
||||
|
||||
func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array:
|
||||
for parameter_set_index in parameter_sets.size():
|
||||
var parameter_set: Array = parameter_sets[parameter_set_index]
|
||||
# run over all function arguments
|
||||
for parameter_index in parameter_set.size():
|
||||
var parameter :Variant = parameter_set[parameter_index]
|
||||
var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index]
|
||||
if parameter is Array:
|
||||
var as_array: Array = parameter
|
||||
# we need to convert the untyped array to the expected typed version
|
||||
if arg_descriptor.is_typed_array():
|
||||
parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null)
|
||||
return parameter_sets
|
||||
|
||||
|
||||
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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://d0q8x2w5alxsx
|
||||
@@ -1,764 +0,0 @@
|
||||
class_name GdScriptParser
|
||||
extends RefCounted
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
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
|
||||
|
||||
|
||||
var TOKEN_NOT_MATCH := Token.new("")
|
||||
var TOKEN_SPACE := SkippableToken.new(" ")
|
||||
var TOKEN_TABULATOR := SkippableToken.new("\t")
|
||||
var TOKEN_NEW_LINE := SkippableToken.new("\n")
|
||||
var TOKEN_COMMENT := SkippableToken.new("#")
|
||||
var TOKEN_CLASS_NAME := RegExToken.new("class_name", GdUnitTools.to_regex("(class_name)\\s+([\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class_name)\\s+([\\w\\p{L}\\p{N}_]+)"), 5)
|
||||
var TOKEN_INNER_CLASS := TokenInnerClass.new("class", GdUnitTools.to_regex("(class)\\s+(\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class)\\s+([\\w\\p{L}\\p{N}_]+)"), 5)
|
||||
var TOKEN_EXTENDS := RegExToken.new("extends", GdUnitTools.to_regex("extends\\s+"))
|
||||
var TOKEN_ENUM := RegExToken.new("enum", GdUnitTools.to_regex("enum\\s+"))
|
||||
var TOKEN_FUNCTION_STATIC_DECLARATION := RegExToken.new("static func", GdUnitTools.to_regex("^static\\s+func\\s+([\\w\\p{L}\\p{N}_]+)"), 1)
|
||||
var TOKEN_FUNCTION_DECLARATION := RegExToken.new("func", GdUnitTools.to_regex("^func\\s+([\\w\\p{L}\\p{N}_]+)"), 1)
|
||||
var TOKEN_FUNCTION := Token.new(".")
|
||||
var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->")
|
||||
var TOKEN_FUNCTION_END := Token.new("):")
|
||||
var TOKEN_ARGUMENT_ASIGNMENT := Token.new("=")
|
||||
var TOKEN_ARGUMENT_TYPE_ASIGNMENT := Token.new(":=")
|
||||
var TOKEN_ARGUMENT_FUZZER := FuzzerToken.new(GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)"))
|
||||
var TOKEN_ARGUMENT_TYPE := Token.new(":")
|
||||
var TOKEN_ARGUMENT_VARIADIC := Token.new("...")
|
||||
var TOKEN_ARGUMENT_SEPARATOR := Token.new(",")
|
||||
var TOKEN_BRACKET_ROUND_OPEN := Token.new("(")
|
||||
var TOKEN_BRACKET_ROUND_CLOSE := Token.new(")")
|
||||
var TOKEN_BRACKET_SQUARE_OPEN := Token.new("[")
|
||||
var TOKEN_BRACKET_SQUARE_CLOSE := Token.new("]")
|
||||
var TOKEN_BRACKET_CURLY_OPEN := Token.new("{")
|
||||
var TOKEN_BRACKET_CURLY_CLOSE := Token.new("}")
|
||||
|
||||
|
||||
var OPERATOR_ADD := Operator.new("+")
|
||||
var OPERATOR_SUB := Operator.new("-")
|
||||
var OPERATOR_MUL := Operator.new("*")
|
||||
var OPERATOR_DIV := Operator.new("/")
|
||||
var OPERATOR_REMAINDER := Operator.new("%")
|
||||
|
||||
var TOKENS :Array[Token] = [
|
||||
TOKEN_SPACE,
|
||||
TOKEN_TABULATOR,
|
||||
TOKEN_NEW_LINE,
|
||||
TOKEN_COMMENT,
|
||||
TOKEN_BRACKET_ROUND_OPEN,
|
||||
TOKEN_BRACKET_ROUND_CLOSE,
|
||||
TOKEN_BRACKET_SQUARE_OPEN,
|
||||
TOKEN_BRACKET_SQUARE_CLOSE,
|
||||
TOKEN_BRACKET_CURLY_OPEN,
|
||||
TOKEN_BRACKET_CURLY_CLOSE,
|
||||
TOKEN_CLASS_NAME,
|
||||
TOKEN_INNER_CLASS,
|
||||
TOKEN_EXTENDS,
|
||||
TOKEN_ENUM,
|
||||
TOKEN_FUNCTION_STATIC_DECLARATION,
|
||||
TOKEN_FUNCTION_DECLARATION,
|
||||
TOKEN_ARGUMENT_FUZZER,
|
||||
TOKEN_ARGUMENT_TYPE_ASIGNMENT,
|
||||
TOKEN_ARGUMENT_ASIGNMENT,
|
||||
TOKEN_ARGUMENT_TYPE,
|
||||
TOKEN_ARGUMENT_VARIADIC,
|
||||
TOKEN_FUNCTION,
|
||||
TOKEN_ARGUMENT_SEPARATOR,
|
||||
TOKEN_FUNCTION_RETURN_TYPE,
|
||||
OPERATOR_ADD,
|
||||
OPERATOR_SUB,
|
||||
OPERATOR_MUL,
|
||||
OPERATOR_DIV,
|
||||
OPERATOR_REMAINDER,
|
||||
]
|
||||
|
||||
var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*")
|
||||
var _scanned_inner_classes := PackedStringArray()
|
||||
var _script_constants := {}
|
||||
var _is_awaiting := GdUnitTools.to_regex("\\bawait\\s+(?![^\"]*\"[^\"]*$)(?!.*#.*await)")
|
||||
|
||||
|
||||
static func to_unix_format(input :String) -> String:
|
||||
return input.replace("\r\n", "\n")
|
||||
|
||||
|
||||
class Token extends RefCounted:
|
||||
var _token: String
|
||||
var _consumed: int
|
||||
var _is_operator: bool
|
||||
|
||||
func _init(p_token: String, p_is_operator := false) -> void:
|
||||
_token = p_token
|
||||
_is_operator = p_is_operator
|
||||
_consumed = p_token.length()
|
||||
|
||||
func match(input: String, pos: int) -> bool:
|
||||
return input.findn(_token, pos) == pos
|
||||
|
||||
func value() -> Variant:
|
||||
return _token
|
||||
|
||||
func is_operator() -> bool:
|
||||
return _is_operator
|
||||
|
||||
func is_inner_class() -> bool:
|
||||
return _token == "class"
|
||||
|
||||
func is_variable() -> bool:
|
||||
return false
|
||||
|
||||
func is_token(token_name :String) -> bool:
|
||||
return _token == token_name
|
||||
|
||||
func is_skippable() -> bool:
|
||||
return false
|
||||
|
||||
func _to_string() -> String:
|
||||
return "Token{" + _token + "}"
|
||||
|
||||
|
||||
class Operator extends Token:
|
||||
func _init(p_value: String) -> void:
|
||||
super(p_value, true)
|
||||
|
||||
func _to_string() -> String:
|
||||
return "OperatorToken{%s}" % [_token]
|
||||
|
||||
|
||||
# A skippable token, is just a placeholder like space or tabs
|
||||
class SkippableToken extends Token:
|
||||
|
||||
func _init(p_token: String) -> void:
|
||||
super(p_token)
|
||||
|
||||
func is_skippable() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# Token to parse function arguments
|
||||
class Variable extends Token:
|
||||
var _plain_value :String
|
||||
var _typed_value :Variant
|
||||
var _type :int = TYPE_NIL
|
||||
|
||||
|
||||
func _init(p_value: String) -> void:
|
||||
super(p_value)
|
||||
_type = _scan_type(p_value)
|
||||
_plain_value = p_value
|
||||
_typed_value = _cast_to_type(p_value, _type)
|
||||
|
||||
|
||||
func _scan_type(p_value: String) -> int:
|
||||
if p_value.begins_with("\"") and p_value.ends_with("\""):
|
||||
return TYPE_STRING
|
||||
var type_ := GdObjects.string_to_type(p_value)
|
||||
if type_ != TYPE_NIL:
|
||||
return type_
|
||||
if p_value.is_valid_int():
|
||||
return TYPE_INT
|
||||
if p_value.is_valid_float():
|
||||
return TYPE_FLOAT
|
||||
if p_value.is_valid_hex_number():
|
||||
return TYPE_INT
|
||||
return TYPE_OBJECT
|
||||
|
||||
|
||||
func _cast_to_type(p_value :String, p_type: int) -> Variant:
|
||||
match p_type:
|
||||
TYPE_STRING:
|
||||
return p_value#.substr(1, p_value.length() - 2)
|
||||
TYPE_INT:
|
||||
return p_value.to_int()
|
||||
TYPE_FLOAT:
|
||||
return p_value.to_float()
|
||||
return p_value
|
||||
|
||||
|
||||
func is_variable() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func type() -> int:
|
||||
return _type
|
||||
|
||||
|
||||
func value() -> Variant:
|
||||
return _typed_value
|
||||
|
||||
|
||||
func plain_value() -> String:
|
||||
return _plain_value
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "Variable{%s: %s : '%s'}" % [_plain_value, GdObjects.type_as_string(_type), _token]
|
||||
|
||||
|
||||
class RegExToken extends Token:
|
||||
var _regex: RegEx
|
||||
var _extract_group_index: int
|
||||
var _value := ""
|
||||
|
||||
|
||||
func _init(token: String, regex: RegEx, extract_group_index: int = -1) -> void:
|
||||
super(token, false)
|
||||
_regex = regex
|
||||
_extract_group_index = extract_group_index
|
||||
|
||||
|
||||
func match(input: String, pos: int) -> bool:
|
||||
var matching := _regex.search(input, pos)
|
||||
if matching == null or pos != matching.get_start():
|
||||
return false
|
||||
if _extract_group_index != -1:
|
||||
_value = matching.get_string(_extract_group_index)
|
||||
_consumed = matching.get_end() - matching.get_start()
|
||||
return true
|
||||
|
||||
|
||||
func value() -> String:
|
||||
return _value
|
||||
|
||||
|
||||
# Token to parse Fuzzers
|
||||
class FuzzerToken extends RegExToken:
|
||||
|
||||
|
||||
func _init(regex: RegEx) -> void:
|
||||
super("fuzzer", regex, 1)
|
||||
|
||||
|
||||
func name() -> String:
|
||||
return value()
|
||||
|
||||
|
||||
func type() -> int:
|
||||
return GdObjects.TYPE_FUZZER
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "FuzzerToken{%s: '%s'}" % [value(), _token]
|
||||
|
||||
|
||||
class TokenInnerClass extends RegExToken:
|
||||
var _content := PackedStringArray()
|
||||
|
||||
|
||||
static func _strip_leading_spaces(input: String) -> String:
|
||||
var characters := input.to_utf8_buffer()
|
||||
while not characters.is_empty():
|
||||
if characters[0] != 0x20:
|
||||
break
|
||||
characters.remove_at(0)
|
||||
return characters.get_string_from_utf8()
|
||||
|
||||
|
||||
static func _consumed_bytes(row: String) -> int:
|
||||
return row.replace(" ", "").replace(" ", "").length()
|
||||
|
||||
|
||||
func _init(token: String, p_regex: RegEx, extract_group_index: int = -1) -> void:
|
||||
super(token, p_regex, extract_group_index)
|
||||
|
||||
|
||||
func is_class_name(clazz_name: String) -> bool:
|
||||
return value() == clazz_name
|
||||
|
||||
|
||||
func content() -> PackedStringArray:
|
||||
return _content
|
||||
|
||||
|
||||
@warning_ignore_start("return_value_discarded")
|
||||
func parse(source_rows: PackedStringArray, offset: int) -> void:
|
||||
# add class signature
|
||||
_content.clear()
|
||||
_content.append(source_rows[offset])
|
||||
# parse class content
|
||||
for row_index in range(offset+1, source_rows.size()):
|
||||
# scan until next non tab
|
||||
var source_row := source_rows[row_index]
|
||||
var row := TokenInnerClass._strip_leading_spaces(source_row)
|
||||
if row.is_empty() or row.begins_with("\t") or row.begins_with("#"):
|
||||
# fold all line to left by removing leading tabs and spaces
|
||||
if source_row.begins_with("\t"):
|
||||
source_row = source_row.trim_prefix("\t")
|
||||
# refomat invalid empty lines
|
||||
if source_row.dedent().is_empty():
|
||||
_content.append("")
|
||||
else:
|
||||
_content.append(source_row)
|
||||
continue
|
||||
break
|
||||
_consumed += TokenInnerClass._consumed_bytes("".join(_content))
|
||||
@warning_ignore_restore("return_value_discarded")
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "TokenInnerClass{%s}" % [value()]
|
||||
|
||||
|
||||
|
||||
func get_token(input: String, current_index: int) -> Token:
|
||||
for t in TOKENS:
|
||||
if t.match(input, current_index):
|
||||
return t
|
||||
return TOKEN_NOT_MATCH
|
||||
|
||||
|
||||
func next_token(input: String, current_index: int, ignore_tokens :Array[Token] = []) -> Token:
|
||||
var token := TOKEN_NOT_MATCH
|
||||
for t :Token in TOKENS.filter(func(t :Token) -> bool: return not ignore_tokens.has(t)):
|
||||
|
||||
if t.match(input, current_index):
|
||||
token = t
|
||||
break
|
||||
if token == OPERATOR_SUB:
|
||||
token = tokenize_value(input, current_index, token)
|
||||
if token == TOKEN_NOT_MATCH:
|
||||
return tokenize_value(input, current_index, token, ignore_tokens.has(TOKEN_FUNCTION))
|
||||
return token
|
||||
|
||||
|
||||
func tokenize_value(input: String, current: int, token: Token, ignore_dots := false) -> Token:
|
||||
var next := 0
|
||||
var current_token := ""
|
||||
# test for '--', '+-', '*-', '/-', '%-', or at least '-x'
|
||||
var test_for_sign := (token == null or token.is_operator()) and input[current] == "-"
|
||||
while current + next < len(input):
|
||||
var character := input[current + next] as String
|
||||
# if first charater a sign
|
||||
# or allowend charset
|
||||
# or is a float value
|
||||
if (test_for_sign and next==0) \
|
||||
or is_allowed_character(character) \
|
||||
or (character == "." and (ignore_dots or current_token.is_valid_int())):
|
||||
current_token += character
|
||||
next += 1
|
||||
continue
|
||||
break
|
||||
if current_token != "":
|
||||
return Variable.new(current_token)
|
||||
return TOKEN_NOT_MATCH
|
||||
|
||||
|
||||
# const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\""
|
||||
func is_allowed_character(input: String) -> bool:
|
||||
var code_point := input.unicode_at(0)
|
||||
# Unicode
|
||||
if code_point > 127:
|
||||
# This is a Unicode character (Chinese, Japanese, etc.)
|
||||
return true
|
||||
# ASCII digit 0-9
|
||||
if code_point >= 48 and code_point <= 57:
|
||||
return true
|
||||
# ASCII lowercase a-z
|
||||
if code_point >= 97 and code_point <= 122:
|
||||
return true
|
||||
# ASCII uppercase A-Z
|
||||
if code_point >= 65 and code_point <= 90:
|
||||
return true
|
||||
# underscore _
|
||||
if code_point == 95:
|
||||
return true
|
||||
# quotes '"
|
||||
if code_point == 34 or code_point == 39:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func parse_return_token(input: String) -> Variable:
|
||||
var index := input.rfind(TOKEN_FUNCTION_RETURN_TYPE._token)
|
||||
if index == -1:
|
||||
return TOKEN_NOT_MATCH
|
||||
index += TOKEN_FUNCTION_RETURN_TYPE._consumed
|
||||
# We scan for the return value exclusive '.' token because it could be referenced to a
|
||||
# external or internal class e.g. 'func foo() -> InnerClass.Bar:'
|
||||
var token := next_token(input, index, [TOKEN_FUNCTION])
|
||||
while !token.is_variable() and token != TOKEN_NOT_MATCH:
|
||||
index += token._consumed
|
||||
token = next_token(input, index, [TOKEN_FUNCTION])
|
||||
return token
|
||||
|
||||
|
||||
func get_function_descriptors(script: GDScript, included_functions: PackedStringArray = []) -> Array[GdFunctionDescriptor]:
|
||||
var fds: Array[GdFunctionDescriptor] = []
|
||||
for method_descriptor in script.get_script_method_list():
|
||||
var func_name: String = method_descriptor["name"]
|
||||
if included_functions.is_empty() or func_name in included_functions:
|
||||
# exclude type set/geters
|
||||
if is_getter_or_setter(func_name):
|
||||
continue
|
||||
if not fds.any(func(fd: GdFunctionDescriptor) -> bool: return fd.name() == func_name):
|
||||
fds.append(GdFunctionDescriptor.extract_from(method_descriptor, false))
|
||||
|
||||
# we need to enrich it by default arguments and line number by parsing the script
|
||||
# the engine core functions has no valid methods to get this info
|
||||
_prescan_script(script)
|
||||
_enrich_function_descriptor(script, fds)
|
||||
return fds
|
||||
|
||||
|
||||
func is_getter_or_setter(func_name: String) -> bool:
|
||||
return func_name.begins_with("@") and (func_name.ends_with("getter") or func_name.ends_with("setter"))
|
||||
|
||||
|
||||
func _parse_function_arguments(input: String) -> Array[Dictionary]:
|
||||
var arguments: Array[Dictionary] = []
|
||||
var current_index := 0
|
||||
var token: Token = null
|
||||
var bracket := 0
|
||||
var in_function := false
|
||||
|
||||
|
||||
while current_index < len(input):
|
||||
token = next_token(input, current_index)
|
||||
# fallback to not end in a endless loop
|
||||
if token == TOKEN_NOT_MATCH:
|
||||
var error : = """
|
||||
Parsing Error: Invalid token at pos %d found.
|
||||
Please report this error!
|
||||
source_code:
|
||||
--------------------------------------------------------------
|
||||
%s
|
||||
--------------------------------------------------------------
|
||||
""".dedent() % [current_index, input]
|
||||
push_error(error)
|
||||
current_index += 1
|
||||
continue
|
||||
current_index += token._consumed
|
||||
if token.is_skippable():
|
||||
continue
|
||||
if token == TOKEN_BRACKET_ROUND_OPEN :
|
||||
in_function = true
|
||||
bracket += 1
|
||||
if token == TOKEN_BRACKET_ROUND_CLOSE:
|
||||
bracket -= 1
|
||||
# if function end?
|
||||
if in_function and bracket == 0:
|
||||
return arguments
|
||||
# is function
|
||||
if token == TOKEN_FUNCTION_DECLARATION:
|
||||
continue
|
||||
|
||||
# is value argument
|
||||
if in_function:
|
||||
var arg_value := ""
|
||||
var current_argument := {
|
||||
"name" : "",
|
||||
"value" : GdFunctionArgument.UNDEFINED,
|
||||
"type" : TYPE_VARIANT
|
||||
}
|
||||
|
||||
# parse type and default value
|
||||
while current_index < len(input):
|
||||
token = next_token(input, current_index)
|
||||
current_index += token._consumed
|
||||
if token.is_skippable():
|
||||
continue
|
||||
|
||||
if token.is_variable() && current_argument["name"] == "":
|
||||
arguments.append(current_argument)
|
||||
current_argument["name"] = (token as Variable).plain_value()
|
||||
continue
|
||||
|
||||
match token:
|
||||
# is fuzzer argument
|
||||
TOKEN_ARGUMENT_FUZZER:
|
||||
arg_value = _parse_end_function(input.substr(current_index), true)
|
||||
current_index += arg_value.length()
|
||||
current_argument["name"] = (token as FuzzerToken).name()
|
||||
current_argument["value"] = arg_value.lstrip(" ")
|
||||
current_argument["type"] = TYPE_FUZZER
|
||||
arguments.append(current_argument)
|
||||
continue
|
||||
|
||||
TOKEN_ARGUMENT_VARIADIC:
|
||||
current_argument["type"] = TYPE_VARARG
|
||||
|
||||
TOKEN_ARGUMENT_TYPE:
|
||||
token = next_token(input, current_index)
|
||||
if token == TOKEN_SPACE:
|
||||
current_index += token._consumed
|
||||
token = next_token(input, current_index)
|
||||
current_index += token._consumed
|
||||
if current_argument["type"] != TYPE_VARARG:
|
||||
current_argument["type"] = GdObjects.string_to_type((token as Variable).plain_value())
|
||||
|
||||
TOKEN_ARGUMENT_TYPE_ASIGNMENT:
|
||||
arg_value = _parse_end_function(input.substr(current_index), true)
|
||||
current_index += arg_value.length()
|
||||
current_argument["value"] = arg_value.lstrip(" ")
|
||||
TOKEN_ARGUMENT_ASIGNMENT:
|
||||
token = next_token(input, current_index)
|
||||
arg_value = _parse_end_function(input.substr(current_index), true)
|
||||
current_index += arg_value.length()
|
||||
current_argument["value"] = arg_value.lstrip(" ")
|
||||
|
||||
TOKEN_BRACKET_SQUARE_OPEN:
|
||||
bracket += 1
|
||||
TOKEN_BRACKET_CURLY_OPEN:
|
||||
bracket += 1
|
||||
TOKEN_BRACKET_ROUND_OPEN :
|
||||
bracket += 1
|
||||
# if value a function?
|
||||
if bracket > 1:
|
||||
# complete the argument value
|
||||
var func_begin := input.substr(current_index-TOKEN_BRACKET_ROUND_OPEN ._consumed)
|
||||
var func_body := _parse_end_function(func_begin)
|
||||
arg_value += func_body
|
||||
# fix parse index to end of value
|
||||
current_index += func_body.length() - TOKEN_BRACKET_ROUND_OPEN ._consumed - TOKEN_BRACKET_ROUND_CLOSE._consumed
|
||||
TOKEN_BRACKET_SQUARE_CLOSE:
|
||||
bracket -= 1
|
||||
TOKEN_BRACKET_CURLY_CLOSE:
|
||||
bracket -= 1
|
||||
TOKEN_BRACKET_ROUND_CLOSE:
|
||||
bracket -= 1
|
||||
# end of function
|
||||
if bracket == 0:
|
||||
break
|
||||
TOKEN_ARGUMENT_SEPARATOR:
|
||||
if bracket <= 1:
|
||||
# next argument
|
||||
current_argument = {
|
||||
"name" : "",
|
||||
"value" : GdFunctionArgument.UNDEFINED,
|
||||
"type" : GdObjects.TYPE_VARIANT
|
||||
}
|
||||
continue
|
||||
return arguments
|
||||
|
||||
|
||||
func _parse_end_function(input: String, remove_trailing_char := false) -> String:
|
||||
# find end of function
|
||||
var current_index := 0
|
||||
var bracket_count := 0
|
||||
var in_array := 0
|
||||
var in_dict := 0
|
||||
var end_of_func := false
|
||||
|
||||
while current_index < len(input) and not end_of_func:
|
||||
var character := input[current_index]
|
||||
# step over strings
|
||||
if character == "'" :
|
||||
current_index = input.find("'", current_index+1) + 1
|
||||
if current_index == 0:
|
||||
push_error("Parsing error on '%s', can't evaluate end of string." % input)
|
||||
return ""
|
||||
continue
|
||||
if character == '"' :
|
||||
# test for string blocks
|
||||
if input.find('"""', current_index) == current_index:
|
||||
current_index = input.find('"""', current_index+3) + 3
|
||||
else:
|
||||
current_index = input.find('"', current_index+1) + 1
|
||||
if current_index == 0:
|
||||
push_error("Parsing error on '%s', can't evaluate end of string." % input)
|
||||
return ""
|
||||
continue
|
||||
|
||||
match character:
|
||||
# count if inside an array
|
||||
"[": in_array += 1
|
||||
"]": in_array -= 1
|
||||
# count if inside an dictionary
|
||||
"{": in_dict += 1
|
||||
"}": in_dict -= 1
|
||||
# count if inside a function
|
||||
"(": bracket_count += 1
|
||||
")":
|
||||
bracket_count -= 1
|
||||
if bracket_count < 0 and in_array <= 0 and in_dict <= 0:
|
||||
end_of_func = true
|
||||
",":
|
||||
if bracket_count == 0 and in_array == 0 and in_dict <= 0:
|
||||
end_of_func = true
|
||||
current_index += 1
|
||||
if remove_trailing_char:
|
||||
# check if the parsed value ends with comma or end of doubled breaked
|
||||
# `<value>,` or `<function>())`
|
||||
var trailing_char := input[current_index-1]
|
||||
if trailing_char == ',' or (bracket_count < 0 and trailing_char == ')'):
|
||||
return input.substr(0, current_index-1)
|
||||
return input.substr(0, current_index)
|
||||
|
||||
|
||||
func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray:
|
||||
for row_index in source_rows.size():
|
||||
var input := source_rows[row_index]
|
||||
var token := next_token(input, 0)
|
||||
if token.is_inner_class():
|
||||
@warning_ignore("unsafe_method_access")
|
||||
if token.is_class_name(clazz_name):
|
||||
@warning_ignore("unsafe_method_access")
|
||||
token.parse(source_rows, row_index)
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return token.content()
|
||||
return PackedStringArray()
|
||||
|
||||
|
||||
func extract_func_signature(rows: PackedStringArray, index: int) -> String:
|
||||
var signature := ""
|
||||
|
||||
for rowIndex in range(index, rows.size()):
|
||||
var row := rows[rowIndex]
|
||||
row = _regex_strip_comments.sub(row, "").strip_edges(false)
|
||||
if row.is_empty():
|
||||
continue
|
||||
signature += row + "\n"
|
||||
if is_func_end(row):
|
||||
return signature.strip_edges()
|
||||
push_error("Can't fully extract function signature of '%s'" % rows[index])
|
||||
return ""
|
||||
|
||||
|
||||
func get_class_name(script :GDScript) -> String:
|
||||
var source_code := GdScriptParser.to_unix_format(script.source_code)
|
||||
var source_rows := source_code.split("\n")
|
||||
|
||||
for index :int in min(10, source_rows.size()):
|
||||
var input := source_rows[index]
|
||||
var token := next_token(input, 0)
|
||||
if token == TOKEN_CLASS_NAME:
|
||||
return token.value()
|
||||
# if no class_name found extract from file name
|
||||
return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file())
|
||||
|
||||
|
||||
func parse_func_name(input: String) -> String:
|
||||
if TOKEN_FUNCTION_DECLARATION.match(input, 0):
|
||||
return TOKEN_FUNCTION_DECLARATION.value()
|
||||
if TOKEN_FUNCTION_STATIC_DECLARATION.match(input, 0):
|
||||
return TOKEN_FUNCTION_STATIC_DECLARATION.value()
|
||||
push_error("Can't extract function name from '%s'" % input)
|
||||
return ""
|
||||
|
||||
|
||||
## Enriches the function descriptor by line number and argument default values
|
||||
## - enrich all function descriptors form current script up to all inherited scrips
|
||||
func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescriptor]) -> void:
|
||||
var enriched_functions := {} # Use Dictionary for O(1) lookup instead of PackedStringArray
|
||||
var script_to_scan := script
|
||||
while script_to_scan != null:
|
||||
# do not scan the test suite base class itself
|
||||
if script_to_scan.resource_path == "res://addons/gdUnit4/src/GdUnitTestSuite.gd":
|
||||
break
|
||||
|
||||
var rows := script_to_scan.source_code.split("\n")
|
||||
for rowIndex in rows.size():
|
||||
var input := rows[rowIndex]
|
||||
# step over inner class functions
|
||||
if input.begins_with("\t"):
|
||||
continue
|
||||
# skip comments and empty lines
|
||||
if input.begins_with("#") or input.length() == 0:
|
||||
continue
|
||||
var token := next_token(input, 0)
|
||||
if token != TOKEN_FUNCTION_STATIC_DECLARATION and token != TOKEN_FUNCTION_DECLARATION:
|
||||
continue
|
||||
|
||||
var function_name: String = token.value()
|
||||
# Skip if already enriched (from parent class scan)
|
||||
if enriched_functions.has(function_name):
|
||||
continue
|
||||
|
||||
# Find matching function descriptor
|
||||
var fd: GdFunctionDescriptor = null
|
||||
for candidate in fds:
|
||||
if candidate.name() == function_name:
|
||||
fd = candidate
|
||||
break
|
||||
if fd == null:
|
||||
continue
|
||||
# Mark as enriched
|
||||
enriched_functions[function_name] = true
|
||||
var func_signature := extract_func_signature(rows, rowIndex)
|
||||
var func_arguments := _parse_function_arguments(func_signature)
|
||||
# enrich missing default values
|
||||
fd.enrich_arguments(func_arguments)
|
||||
fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1)
|
||||
fd._is_coroutine = is_func_coroutine(rows, rowIndex)
|
||||
# enrich return class name if not set
|
||||
if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]:
|
||||
var var_token := parse_return_token(func_signature)
|
||||
if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT:
|
||||
fd._return_class = _patch_inner_class_names(var_token.plain_value(), "")
|
||||
# if the script ihnerits we need to scan this also
|
||||
script_to_scan = script_to_scan.get_base_script()
|
||||
|
||||
|
||||
func is_func_coroutine(rows :PackedStringArray, index :int) -> bool:
|
||||
var is_coroutine := false
|
||||
for rowIndex in range(index+1, rows.size()):
|
||||
var input := rows[rowIndex].strip_edges()
|
||||
if input.begins_with("#") or input.is_empty():
|
||||
continue
|
||||
var token := next_token(input, 0)
|
||||
# scan until next function
|
||||
if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION:
|
||||
break
|
||||
|
||||
if _is_awaiting.search(input):
|
||||
return true
|
||||
return is_coroutine
|
||||
|
||||
|
||||
func is_inner_class(clazz_path :PackedStringArray) -> bool:
|
||||
return clazz_path.size() > 1
|
||||
|
||||
|
||||
func is_func_end(row :String) -> bool:
|
||||
return row.strip_edges(false, true).ends_with(":")
|
||||
|
||||
|
||||
func _patch_inner_class_names(clazz :String, clazz_name :String = "") -> String:
|
||||
var inner_clazz_name := clazz.split(".")[0]
|
||||
if _scanned_inner_classes.has(inner_clazz_name):
|
||||
return inner_clazz_name
|
||||
#var base_clazz := clazz_name.split(".")[0]
|
||||
#return base_clazz + "." + clazz
|
||||
if _script_constants.has(clazz):
|
||||
return clazz_name + "." + clazz
|
||||
return clazz
|
||||
|
||||
|
||||
func _prescan_script(script: GDScript) -> void:
|
||||
_script_constants = script.get_script_constant_map()
|
||||
for key :String in _script_constants.keys():
|
||||
var value :Variant = _script_constants.get(key)
|
||||
if value is GDScript:
|
||||
@warning_ignore("return_value_discarded")
|
||||
_scanned_inner_classes.append(key)
|
||||
|
||||
|
||||
func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult:
|
||||
if clazz_path.is_empty():
|
||||
return GdUnitResult.error("Invalid script path '%s'" % clazz_path)
|
||||
var is_inner_class_ := is_inner_class(clazz_path)
|
||||
var script :GDScript = load(clazz_path[0])
|
||||
_prescan_script(script)
|
||||
|
||||
if is_inner_class_:
|
||||
var inner_class_name := clazz_path[1]
|
||||
if _scanned_inner_classes.has(inner_class_name):
|
||||
# do load only on inner class source code and enrich the stored script instance
|
||||
var source_code := _load_inner_class(script, inner_class_name)
|
||||
script = _script_constants.get(inner_class_name)
|
||||
script.source_code = source_code
|
||||
var function_descriptors := get_function_descriptors(script)
|
||||
var gd_class := GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors)
|
||||
return GdUnitResult.success(gd_class)
|
||||
|
||||
|
||||
func _load_inner_class(script: GDScript, inner_clazz: String) -> String:
|
||||
var source_rows := GdScriptParser.to_unix_format(script.source_code).split("\n")
|
||||
# extract all inner class names
|
||||
var inner_class_code := extract_inner_class(source_rows, inner_clazz)
|
||||
return "\n".join(inner_class_code)
|
||||
@@ -1 +0,0 @@
|
||||
uid://47c36e540hgy
|
||||
@@ -1,74 +0,0 @@
|
||||
class_name GdUnitExpressionRunner
|
||||
extends RefCounted
|
||||
|
||||
const CLASS_TEMPLATE = """
|
||||
class_name _ExpressionRunner extends '${clazz_path}'
|
||||
|
||||
func __run_expression() -> Variant:
|
||||
return $expression
|
||||
|
||||
"""
|
||||
|
||||
var constructor_args_regex := RegEx.create_from_string("new\\((?<args>.*)\\)")
|
||||
|
||||
|
||||
func execute(src_script: GDScript, value: Variant) -> Variant:
|
||||
if typeof(value) != TYPE_STRING:
|
||||
return value
|
||||
|
||||
var expression: String = value
|
||||
var parameter_map := src_script.get_script_constant_map()
|
||||
for key: String in parameter_map.keys():
|
||||
var parameter_value: Variant = parameter_map[key]
|
||||
# check we need to construct from inner class
|
||||
# we need to use the original class instance from the script_constant_map otherwise we run into a runtime error
|
||||
if expression.begins_with(key + ".new") and parameter_value is GDScript:
|
||||
var object: GDScript = parameter_value
|
||||
var args := build_constructor_arguments(parameter_map, expression.substr(expression.find("new")))
|
||||
if args.is_empty():
|
||||
return object.new()
|
||||
return object.callv("new", args)
|
||||
|
||||
var script := GDScript.new()
|
||||
var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path
|
||||
script.source_code = CLASS_TEMPLATE.dedent()\
|
||||
.replace("${clazz_path}", resource_path)\
|
||||
.replace("$expression", expression)
|
||||
#script.take_over_path(resource_path)
|
||||
@warning_ignore("return_value_discarded")
|
||||
script.reload(true)
|
||||
var runner: Object = script.new()
|
||||
if runner.has_method("queue_free"):
|
||||
(runner as Node).queue_free()
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return runner.__run_expression()
|
||||
|
||||
|
||||
func build_constructor_arguments(parameter_map: Dictionary, expression: String) -> Array[Variant]:
|
||||
var result := constructor_args_regex.search(expression)
|
||||
var extracted_arguments := result.get_string("args").strip_edges()
|
||||
if extracted_arguments.is_empty():
|
||||
return []
|
||||
var arguments :Array = extracted_arguments.split(",")
|
||||
return arguments.map(func(argument: String) -> Variant:
|
||||
var value := argument.strip_edges()
|
||||
|
||||
# is argument an constant value
|
||||
if parameter_map.has(value):
|
||||
return parameter_map[value]
|
||||
# is typed named value like Vector3.ONE
|
||||
for type:int in GdObjects.TYPE_AS_STRING_MAPPINGS:
|
||||
var type_as_string:String = GdObjects.TYPE_AS_STRING_MAPPINGS[type]
|
||||
if value.begins_with(type_as_string):
|
||||
return type_convert(value, type)
|
||||
# is value a string
|
||||
if value.begins_with("'") or value.begins_with('"'):
|
||||
return value.trim_prefix("'").trim_suffix("'").trim_prefix('"').trim_suffix('"')
|
||||
# fallback to default value converting
|
||||
return str_to_var(value)
|
||||
)
|
||||
|
||||
|
||||
func to_fuzzer(src_script: GDScript, expression: String) -> Fuzzer:
|
||||
@warning_ignore("unsafe_cast")
|
||||
return execute(src_script, expression) as Fuzzer
|
||||
@@ -1 +0,0 @@
|
||||
uid://8gc4dp0ot52d
|
||||
@@ -1,163 +0,0 @@
|
||||
## @deprecated see GdFunctionParameterSetResolver
|
||||
class_name GdUnitTestParameterSetResolver
|
||||
extends RefCounted
|
||||
|
||||
const CLASS_TEMPLATE = """
|
||||
class_name _ParameterExtractor extends '${clazz_path}'
|
||||
|
||||
func __extract_test_parameters() -> Array:
|
||||
return ${test_params}
|
||||
|
||||
"""
|
||||
|
||||
const EXCLUDE_PROPERTIES_TO_COPY = [
|
||||
"script",
|
||||
"type",
|
||||
"Node",
|
||||
"_import_path"]
|
||||
|
||||
|
||||
var _fd: GdFunctionDescriptor
|
||||
var _static_sets_by_index := {}
|
||||
var _is_static := true
|
||||
|
||||
func _init(fd: GdFunctionDescriptor) -> void:
|
||||
_fd = fd
|
||||
|
||||
|
||||
func is_parameterized() -> bool:
|
||||
return _fd.is_parameterized()
|
||||
|
||||
|
||||
func is_parameter_sets_static() -> bool:
|
||||
return _is_static
|
||||
|
||||
|
||||
func is_parameter_set_static(index: int) -> bool:
|
||||
return _is_static and _static_sets_by_index.get(index, false)
|
||||
|
||||
|
||||
# validates the given arguments are complete and matches to required input fields of the test function
|
||||
func validate(parameter_sets: Array, parameter_set_index: int) -> GdUnitResult:
|
||||
if parameter_sets.size() < parameter_set_index:
|
||||
return GdUnitResult.error("Internal error: the resolved paremeterset has invalid size.")
|
||||
|
||||
var input_values: Array = parameter_sets[parameter_set_index]
|
||||
if input_values == null:
|
||||
return GdUnitResult.error("The parameter set '%s' must be an Array!" % parameter_sets[parameter_set_index])
|
||||
|
||||
# check given parameter set with test case arguments
|
||||
var input_arguments := _fd.args()
|
||||
var expected_arg_count := input_arguments.size() - 1 #(-1 we exclude the parameter set itself)
|
||||
var current_arg_count := input_values.size()
|
||||
if current_arg_count != expected_arg_count:
|
||||
var arg_names := input_arguments\
|
||||
.filter(func(arg: GdFunctionArgument) -> bool: return not arg.is_parameter_set())\
|
||||
.map(func(arg: GdFunctionArgument) -> String: return str(arg))
|
||||
|
||||
return GdUnitResult.error("""
|
||||
The test data set at index (%d) does not match the expected test arguments:
|
||||
test function: [color=snow]func test...(%s)[/color]
|
||||
test input values: [color=snow]%s[/color]
|
||||
"""
|
||||
.dedent() % [parameter_set_index, ",".join(arg_names), input_values])
|
||||
return GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, input_values)
|
||||
|
||||
|
||||
static func validate_parameter_types(input_arguments: Array[GdFunctionArgument], input_values: Array) -> GdUnitResult:
|
||||
for i in input_arguments.size():
|
||||
var input_param: GdFunctionArgument = input_arguments[i]
|
||||
# only check the test input arguments
|
||||
if input_param.is_parameter_set():
|
||||
continue
|
||||
var input_param_type := input_param.type()
|
||||
var input_value :Variant = input_values[i]
|
||||
var input_value_type := typeof(input_value)
|
||||
# input parameter is not typed or is Variant we skip the type test
|
||||
if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT:
|
||||
continue
|
||||
# is input type enum allow int values
|
||||
if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT:
|
||||
continue
|
||||
# allow only equal types and object == null
|
||||
if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL:
|
||||
continue
|
||||
if input_param_type != input_value_type:
|
||||
return GdUnitResult.error("""
|
||||
The test data value does not match the expected input type!
|
||||
input value: [color=snow]'%s', <%s>[/color]
|
||||
expected argument: [color=snow]%s[/color]
|
||||
"""
|
||||
.dedent() % [input_value, type_string(input_value_type), str(input_param)])
|
||||
return GdUnitResult.success("No errors found.")
|
||||
|
||||
|
||||
func _extract_property_names(node :Node) -> PackedStringArray:
|
||||
return node.get_property_list()\
|
||||
.map(func(property :Dictionary) -> String: return property["name"])\
|
||||
.filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property))
|
||||
|
||||
|
||||
# tests if the test property set contains an property reference by name, if not the parameter set holds only static values
|
||||
func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool:
|
||||
for property_name in property_names:
|
||||
if parameters.contains(property_name):
|
||||
_is_static = false
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# extracts the arguments from the given test case, using kind of reflection solution
|
||||
# to restore the parameters from a string representation to real instance type
|
||||
func load_parameter_sets(test_suite: Node) -> GdUnitResult:
|
||||
var source_script: Script = test_suite.get_script()
|
||||
var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args())
|
||||
var source_code := CLASS_TEMPLATE \
|
||||
.replace("${clazz_path}", source_script.resource_path) \
|
||||
.replace("${test_params}", parameter_arg.value_as_string())
|
||||
var script := GDScript.new()
|
||||
script.source_code = source_code
|
||||
# enable this lines only for debuging
|
||||
#script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name()
|
||||
#DirAccess.remove_absolute(script.resource_path)
|
||||
#ResourceSaver.save(script, script.resource_path)
|
||||
var result := script.reload()
|
||||
if result != OK:
|
||||
return GdUnitResult.error("Extracting test parameters failed! Script loading error: %s" % error_string(result))
|
||||
var instance :Object = script.new()
|
||||
GdUnitTestParameterSetResolver.copy_properties(test_suite, instance)
|
||||
(instance as Node).queue_free()
|
||||
var parameter_sets: Array = instance.call("__extract_test_parameters")
|
||||
fixure_typed_parameters(parameter_sets, _fd.args())
|
||||
return GdUnitResult.success(parameter_sets)
|
||||
|
||||
|
||||
func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array:
|
||||
for parameter_set_index in parameter_sets.size():
|
||||
var parameter_set: Array = parameter_sets[parameter_set_index]
|
||||
# run over all function arguments
|
||||
for parameter_index in parameter_set.size():
|
||||
var parameter :Variant = parameter_set[parameter_index]
|
||||
var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index]
|
||||
if parameter is Array:
|
||||
var as_array: Array = parameter
|
||||
# we need to convert the untyped array to the expected typed version
|
||||
if arg_descriptor.is_typed_array():
|
||||
parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null)
|
||||
return parameter_sets
|
||||
|
||||
|
||||
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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://buij3yet6d2hg
|
||||
Reference in New Issue
Block a user