reinstalling GDUnit from assetlib
Some checks failed
Create tag and build when new code gets to main / Export (push) Failing after 6m41s
Some checks failed
Create tag and build when new code gets to main / Export (push) Failing after 6m41s
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
## Small helper tool to work with Godot Arrays
|
||||
class_name GdArrayTools
|
||||
extends RefCounted
|
||||
|
||||
|
||||
const max_elements := 32
|
||||
const ARRAY_TYPES := [
|
||||
TYPE_ARRAY,
|
||||
TYPE_PACKED_BYTE_ARRAY,
|
||||
TYPE_PACKED_INT32_ARRAY,
|
||||
TYPE_PACKED_INT64_ARRAY,
|
||||
TYPE_PACKED_FLOAT32_ARRAY,
|
||||
TYPE_PACKED_FLOAT64_ARRAY,
|
||||
TYPE_PACKED_STRING_ARRAY,
|
||||
TYPE_PACKED_VECTOR2_ARRAY,
|
||||
TYPE_PACKED_VECTOR3_ARRAY,
|
||||
TYPE_PACKED_VECTOR4_ARRAY,
|
||||
TYPE_PACKED_COLOR_ARRAY
|
||||
]
|
||||
|
||||
|
||||
static func is_array_type(value: Variant) -> bool:
|
||||
return is_type_array(typeof(value))
|
||||
|
||||
|
||||
static func is_type_array(type :int) -> bool:
|
||||
return type in ARRAY_TYPES
|
||||
|
||||
|
||||
## Filters an array by given value[br]
|
||||
## If the given value not an array it returns null, will remove all occurence of given value.
|
||||
static func filter_value(array: Variant, value: Variant) -> Variant:
|
||||
if not is_array_type(array):
|
||||
return null
|
||||
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var filtered_array: Variant = array.duplicate()
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var index: int = filtered_array.find(value)
|
||||
while index != -1:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
filtered_array.remove_at(index)
|
||||
@warning_ignore("unsafe_method_access")
|
||||
index = filtered_array.find(value)
|
||||
return filtered_array
|
||||
|
||||
|
||||
## Groups an array by a custom key selector
|
||||
## The function should take an item and return the group key
|
||||
static func group_by(array: Array, key_selector: Callable) -> Dictionary:
|
||||
var result := {}
|
||||
|
||||
for item: Variant in array:
|
||||
var group_key: Variant = key_selector.call(item)
|
||||
var values: Array = result.get_or_add(group_key, [])
|
||||
values.append(item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Erases a value from given array by using equals(l,r) to find the element to erase
|
||||
static func erase_value(array :Array, value :Variant) -> void:
|
||||
for element :Variant in array:
|
||||
if GdObjects.equals(element, value):
|
||||
array.erase(element)
|
||||
|
||||
|
||||
## Scans for the array build in type on a untyped array[br]
|
||||
## Returns the buildin type by scan all values and returns the type if all values has the same type.
|
||||
## If the values has different types TYPE_VARIANT is returend
|
||||
static func scan_typed(array :Array) -> int:
|
||||
if array.is_empty():
|
||||
return TYPE_NIL
|
||||
var actual_type := GdObjects.TYPE_VARIANT
|
||||
for value :Variant in array:
|
||||
var current_type := typeof(value)
|
||||
if not actual_type in [GdObjects.TYPE_VARIANT, current_type]:
|
||||
return GdObjects.TYPE_VARIANT
|
||||
actual_type = current_type
|
||||
return actual_type
|
||||
|
||||
|
||||
## Converts given array into a string presentation.[br]
|
||||
## This function is different to the original Godot str(<array>) implementation.
|
||||
## The string presentaion contains fullquallified typed informations.
|
||||
##[br]
|
||||
## Examples:
|
||||
## [codeblock]
|
||||
## # will result in PackedString(["a", "b"])
|
||||
## GdArrayTools.as_string(PackedStringArray("a", "b"))
|
||||
## # will result in PackedString(["a", "b"])
|
||||
## GdArrayTools.as_string(PackedColorArray(Color.RED, COLOR.GREEN))
|
||||
## [/codeblock]
|
||||
static func as_string(elements: Variant, encode_value := true) -> String:
|
||||
var delemiter := ", "
|
||||
if elements == null:
|
||||
return "<null>"
|
||||
@warning_ignore("unsafe_cast")
|
||||
if (elements as Array).is_empty():
|
||||
return "<empty>"
|
||||
var prefix := _typeof_as_string(elements) if encode_value else ""
|
||||
var formatted := ""
|
||||
var index := 0
|
||||
for element :Variant in elements:
|
||||
if max_elements != -1 and index > max_elements:
|
||||
return prefix + "[" + formatted + delemiter + "...]"
|
||||
if formatted.length() > 0 :
|
||||
formatted += delemiter
|
||||
formatted += GdDefaultValueDecoder.decode(element) if encode_value else str(element)
|
||||
index += 1
|
||||
return prefix + "[" + formatted + "]"
|
||||
|
||||
|
||||
static func has_same_content(current: Array, other: Array) -> bool:
|
||||
if current.size() != other.size(): return false
|
||||
for element: Variant in current:
|
||||
if not other.has(element): return false
|
||||
if current.count(element) != other.count(element): return false
|
||||
return true
|
||||
|
||||
|
||||
static func _typeof_as_string(value :Variant) -> String:
|
||||
var type := typeof(value)
|
||||
# for untyped array we retun empty string
|
||||
if type == TYPE_ARRAY:
|
||||
return ""
|
||||
return GdObjects.typeof_as_string(value)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bk60ywsj4ekp7
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# Myers' Diff Algorithm implementation
|
||||
# Based on "An O(ND) Difference Algorithm and Its Variations" by Eugene W. Myers
|
||||
class_name GdDiffTool
|
||||
extends RefCounted
|
||||
|
||||
|
||||
const DIV_ADD :int = 214
|
||||
const DIV_SUB :int = 215
|
||||
|
||||
|
||||
class Edit:
|
||||
enum Type { EQUAL, INSERT, DELETE }
|
||||
var type: Type
|
||||
var character: int
|
||||
|
||||
func _init(t: Type, chr: int) -> void:
|
||||
type = t
|
||||
character = chr
|
||||
|
||||
|
||||
# Main entry point - returns [ldiff, rdiff]
|
||||
static func string_diff(left: Variant, right: Variant) -> Array[PackedInt32Array]:
|
||||
var lb := PackedInt32Array() if left == null else str(left).to_utf32_buffer().to_int32_array()
|
||||
var rb := PackedInt32Array() if right == null else str(right).to_utf32_buffer().to_int32_array()
|
||||
|
||||
# Early exit for identical strings
|
||||
if lb == rb:
|
||||
return [lb.duplicate(), rb.duplicate()]
|
||||
|
||||
var edits := _myers_diff(lb, rb)
|
||||
return _edits_to_diff_format(edits)
|
||||
|
||||
|
||||
# Core Myers' algorithm
|
||||
static func _myers_diff(a: PackedInt32Array, b: PackedInt32Array) -> Array[Edit]:
|
||||
var n := a.size()
|
||||
var m := b.size()
|
||||
var max_d := n + m
|
||||
|
||||
# V array stores the furthest reaching x coordinate for each k-line
|
||||
# We need indices from -max_d to max_d, so we offset by max_d
|
||||
var v := PackedInt32Array()
|
||||
v.resize(2 * max_d + 1)
|
||||
v.fill(-1)
|
||||
v[max_d + 1] = 0 # k=1 starts at x=0
|
||||
|
||||
var trace := [] # Store V arrays for each d to backtrack later
|
||||
|
||||
# Find the edit distance
|
||||
for d in range(0, max_d + 1):
|
||||
# Store current V for backtracking
|
||||
trace.append(v.duplicate())
|
||||
|
||||
for k in range(-d, d + 1, 2):
|
||||
var k_offset := k + max_d
|
||||
|
||||
# Decide whether to move down or right
|
||||
var x: int
|
||||
if k == -d or (k != d and v[k_offset - 1] < v[k_offset + 1]):
|
||||
x = v[k_offset + 1] # Move down (insert from b)
|
||||
else:
|
||||
x = v[k_offset - 1] + 1 # Move right (delete from a)
|
||||
|
||||
var y := x - k
|
||||
|
||||
# Follow diagonal as far as possible (matching characters)
|
||||
while x < n and y < m and a[x] == b[y]:
|
||||
x += 1
|
||||
y += 1
|
||||
|
||||
v[k_offset] = x
|
||||
|
||||
# Check if we've reached the end
|
||||
if x >= n and y >= m:
|
||||
return _backtrack(a, b, trace, d, max_d)
|
||||
|
||||
# Should never reach here for valid inputs
|
||||
return []
|
||||
|
||||
|
||||
# Backtrack through the edit graph to build the edit script
|
||||
static func _backtrack(a: PackedInt32Array, b: PackedInt32Array, trace: Array, d: int, max_d: int) -> Array[Edit]:
|
||||
var edits: Array[Edit] = []
|
||||
var x := a.size()
|
||||
var y := b.size()
|
||||
|
||||
# Walk backwards through each d value
|
||||
for depth in range(d, -1, -1):
|
||||
var v: PackedInt32Array = trace[depth]
|
||||
var k := x - y
|
||||
var k_offset := k + max_d
|
||||
|
||||
# Determine previous k
|
||||
var prev_k: int
|
||||
if k == -depth or (k != depth and v[k_offset - 1] < v[k_offset + 1]):
|
||||
prev_k = k + 1
|
||||
else:
|
||||
prev_k = k - 1
|
||||
|
||||
var prev_k_offset := prev_k + max_d
|
||||
var prev_x := v[prev_k_offset]
|
||||
var prev_y := prev_x - prev_k
|
||||
|
||||
# Extract diagonal (equal) characters
|
||||
while x > prev_x and y > prev_y:
|
||||
x -= 1
|
||||
y -= 1
|
||||
#var char_array := PackedInt32Array([a[x]])
|
||||
edits.insert(0, Edit.new(Edit.Type.EQUAL, a[x]))
|
||||
|
||||
# Record the edit operation
|
||||
if depth > 0:
|
||||
if x == prev_x:
|
||||
# Insert from b
|
||||
y -= 1
|
||||
#var char_array := PackedInt32Array([b[y]])
|
||||
edits.insert(0, Edit.new(Edit.Type.INSERT, b[y]))
|
||||
else:
|
||||
# Delete from a
|
||||
x -= 1
|
||||
#var char_array := PackedInt32Array([a[x]])
|
||||
edits.insert(0, Edit.new(Edit.Type.DELETE, a[x]))
|
||||
|
||||
return edits
|
||||
|
||||
|
||||
# Convert edit script to the DIV_ADD/DIV_SUB format
|
||||
static func _edits_to_diff_format(edits: Array[Edit]) -> Array[PackedInt32Array]:
|
||||
var ldiff := PackedInt32Array()
|
||||
var rdiff := PackedInt32Array()
|
||||
|
||||
for edit in edits:
|
||||
match edit.type:
|
||||
Edit.Type.EQUAL:
|
||||
ldiff.append(edit.character)
|
||||
rdiff.append(edit.character)
|
||||
Edit.Type.INSERT:
|
||||
ldiff.append(DIV_ADD)
|
||||
ldiff.append(edit.character)
|
||||
rdiff.append(DIV_SUB)
|
||||
rdiff.append(edit.character)
|
||||
Edit.Type.DELETE:
|
||||
ldiff.append(DIV_SUB)
|
||||
ldiff.append(edit.character)
|
||||
rdiff.append(DIV_ADD)
|
||||
rdiff.append(edit.character)
|
||||
|
||||
return [ldiff, rdiff]
|
||||
|
||||
|
||||
# prototype
|
||||
static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStringArray:
|
||||
var text1Words := text1.split(" ")
|
||||
var text2Words := text2.split(" ")
|
||||
var text1WordCount := text1Words.size()
|
||||
var text2WordCount := text2Words.size()
|
||||
var solutionMatrix := Array()
|
||||
for i in text1WordCount+1:
|
||||
var ar := Array()
|
||||
for n in text2WordCount+1:
|
||||
ar.append(0)
|
||||
solutionMatrix.append(ar)
|
||||
|
||||
for i in range(text1WordCount-1, 0, -1):
|
||||
for j in range(text2WordCount-1, 0, -1):
|
||||
if text1Words[i] == text2Words[j]:
|
||||
solutionMatrix[i][j] = solutionMatrix[i + 1][j + 1] + 1;
|
||||
else:
|
||||
solutionMatrix[i][j] = max(solutionMatrix[i + 1][j], solutionMatrix[i][j + 1]);
|
||||
|
||||
var i := 0
|
||||
var j := 0
|
||||
var lcsResultList := PackedStringArray();
|
||||
while (i < text1WordCount && j < text2WordCount):
|
||||
if text1Words[i] == text2Words[j]:
|
||||
@warning_ignore("return_value_discarded")
|
||||
lcsResultList.append(text2Words[j])
|
||||
i += 1
|
||||
j += 1
|
||||
else: if (solutionMatrix[i + 1][j] >= solutionMatrix[i][j + 1]):
|
||||
i += 1
|
||||
else:
|
||||
j += 1
|
||||
return lcsResultList
|
||||
|
||||
|
||||
static func markTextDifferences(text1 :String, text2 :String, lcsList :PackedStringArray, insertColor :Color, deleteColor:Color) -> String:
|
||||
var stringBuffer := ""
|
||||
if text1 == null and lcsList == null:
|
||||
return stringBuffer
|
||||
|
||||
var text1Words := text1.split(" ")
|
||||
var text2Words := text2.split(" ")
|
||||
var i := 0
|
||||
var j := 0
|
||||
var word1LastIndex := 0
|
||||
var word2LastIndex := 0
|
||||
for k in lcsList.size():
|
||||
while i < text1Words.size() and j < text2Words.size():
|
||||
if text1Words[i] == lcsList[k] and text2Words[j] == lcsList[k]:
|
||||
stringBuffer += "<SPAN>" + lcsList[k] + " </SPAN>"
|
||||
word1LastIndex = i + 1
|
||||
word2LastIndex = j + 1
|
||||
i = text1Words.size()
|
||||
j = text2Words.size()
|
||||
|
||||
else: if text1Words[i] != lcsList[k]:
|
||||
while i < text1Words.size() and text1Words[i] != lcsList[k]:
|
||||
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + deleteColor.to_html() + "'>" + text1Words[i] + " </SPAN>"
|
||||
i += 1
|
||||
else: if text2Words[j] != lcsList[k]:
|
||||
while j < text2Words.size() and text2Words[j] != lcsList[k]:
|
||||
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + insertColor.to_html() + "'>" + text2Words[j] + " </SPAN>"
|
||||
j += 1
|
||||
i = word1LastIndex
|
||||
j = word2LastIndex
|
||||
|
||||
while word1LastIndex < text1Words.size():
|
||||
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + deleteColor.to_html() + "'>" + text1Words[word1LastIndex] + " </SPAN>"
|
||||
word1LastIndex += 1
|
||||
while word2LastIndex < text2Words.size():
|
||||
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + insertColor.to_html() + "'>" + text2Words[word2LastIndex] + " </SPAN>"
|
||||
word2LastIndex += 1
|
||||
return stringBuffer
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5sli0lem5xca
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
# 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")
|
||||
|
||||
|
||||
# introduced with Godot 4.3.beta1
|
||||
const TYPE_VOID = 1000
|
||||
const TYPE_VARARG = 1001
|
||||
const TYPE_VARIANT = 1002
|
||||
const TYPE_FUNC = 1003
|
||||
const TYPE_FUZZER = 1004
|
||||
# missing Godot types
|
||||
const TYPE_NODE = 2001
|
||||
const TYPE_CONTROL = 2002
|
||||
const TYPE_CANVAS = 2003
|
||||
const TYPE_ENUM = 2004
|
||||
|
||||
|
||||
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_VECTOR4_ARRAY: "PackedVector4Array",
|
||||
TYPE_PACKED_COLOR_ARRAY: "PackedColorArray",
|
||||
TYPE_VOID: "void",
|
||||
TYPE_VARARG: "VarArg",
|
||||
TYPE_FUNC: "Func",
|
||||
TYPE_FUZZER: "Fuzzer",
|
||||
TYPE_VARIANT: "Variant"
|
||||
}
|
||||
|
||||
|
||||
class EditorNotifications:
|
||||
# NOTE: Hardcoding to avoid runtime errors in exported projects when editor
|
||||
# classes are not available. These values are unlikely to change.
|
||||
# See: EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED
|
||||
const NOTIFICATION_EDITOR_SETTINGS_CHANGED := 10000
|
||||
|
||||
|
||||
const NOTIFICATION_AS_STRING_MAPPINGS := {
|
||||
TYPE_OBJECT: {
|
||||
Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE",
|
||||
Object.NOTIFICATION_PREDELETE: "PREDELETE",
|
||||
EditorNotifications.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED",
|
||||
},
|
||||
TYPE_NODE: {
|
||||
Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE",
|
||||
Node.NOTIFICATION_EXIT_TREE: "EXIT_TREE",
|
||||
Node.NOTIFICATION_CHILD_ORDER_CHANGED: "CHILD_ORDER_CHANGED",
|
||||
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 : {
|
||||
Object.NOTIFICATION_PREDELETE: "PREDELETE",
|
||||
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 script: Script = obj.get_script()
|
||||
# handle build-in scripts
|
||||
if script.resource_path != null and script.resource_path.contains(".tscn"):
|
||||
var path_elements := script.resource_path.split(".tscn")
|
||||
clazz_name = path_elements[0].get_file()
|
||||
clazz_path = script.resource_path
|
||||
else:
|
||||
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 :String = property["name"]
|
||||
var property_type :int = property["type"]
|
||||
var property_value :Variant = obj.get(property_name)
|
||||
if property_value is GDScript or property_value is Callable or property_value is RegEx:
|
||||
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
|
||||
@warning_ignore("unsafe_cast")
|
||||
dict[property_name] = obj2dict(property_value as Object, hashed_objects)
|
||||
else:
|
||||
dict[property_name] = property_value
|
||||
if obj is Node:
|
||||
var childrens :Array = (obj as Node).get_children()
|
||||
dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
|
||||
if obj is TreeItem:
|
||||
var childrens :Array = (obj as TreeItem).get_children()
|
||||
dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
|
||||
|
||||
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[Variant], obj_b: Array[Variant], case_sensitive: bool = false, compare_mode: COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
|
||||
var a: Array[Variant] = obj_a.duplicate()
|
||||
var b: Array[Variant] = 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:
|
||||
@warning_ignore("unsafe_cast")
|
||||
return (obj_a as GdUnitArgumentMatcher).is_match(obj_b)
|
||||
if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher:
|
||||
@warning_ignore("unsafe_cast")
|
||||
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
|
||||
@warning_ignore("unsafe_method_access")
|
||||
if obj_a.get_class() != obj_b.get_class():
|
||||
return false
|
||||
@warning_ignore("unsafe_cast")
|
||||
var a := obj2dict(obj_a as Object)
|
||||
@warning_ignore("unsafe_cast")
|
||||
var b := obj2dict(obj_b as Object)
|
||||
return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth)
|
||||
return obj_a == obj_b
|
||||
|
||||
TYPE_ARRAY:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
if obj_a.size() != obj_b.size():
|
||||
return false
|
||||
@warning_ignore("unsafe_method_access")
|
||||
for index :int 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:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
if obj_a.size() != obj_b.size():
|
||||
return false
|
||||
@warning_ignore("unsafe_method_access")
|
||||
for key :Variant in obj_a.keys():
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var value_a :Variant = obj_a[key] if obj_a.has(key) else null
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var value_b :Variant = 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:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
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 and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].has(notification):
|
||||
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].get(notification, error)
|
||||
if instance is Control and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].has(notification):
|
||||
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 :int 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(" ", "")
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
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:
|
||||
if type < TYPE_MAX:
|
||||
return type_string(type)
|
||||
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 :Variant) -> bool:
|
||||
if value is GDScript or value is ScriptExtension:
|
||||
return false
|
||||
var obj: Object = value
|
||||
if is_instance_valid(obj) and obj.has_method("is_class"):
|
||||
return obj.is_class("GDScriptNativeClass")
|
||||
return false
|
||||
|
||||
|
||||
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
|
||||
@warning_ignore("unsafe_cast")
|
||||
if value is GDScript and (value as GDScript).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:
|
||||
@warning_ignore("unsafe_cast")
|
||||
return (left as Object).get_instance_id() == (right as Object).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_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:
|
||||
@warning_ignore("unsafe_cast")
|
||||
return value is String and (value as String).ends_with(".tscn")
|
||||
|
||||
|
||||
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():
|
||||
@warning_ignore("unsafe_cast")
|
||||
if (value as Object).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
|
||||
@warning_ignore("unsafe_cast")
|
||||
if is_script(value) and (value as Script).get_instance_base_type() == "":
|
||||
return true
|
||||
if is_scene(value):
|
||||
return true
|
||||
@warning_ignore("unsafe_cast")
|
||||
return not (value as Object).has_method('new') and not (value as Object).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: Node = instance
|
||||
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
|
||||
@warning_ignore("unsafe_cast")
|
||||
return (obj as Object).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)
|
||||
@warning_ignore("unsafe_method_access")
|
||||
return GdUnitResult.success(clazz.new())
|
||||
TYPE_STRING:
|
||||
var clazz_name: String = clazz
|
||||
if ClassDB.class_exists(clazz_name):
|
||||
if Engine.has_singleton(clazz_name):
|
||||
return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz_name)
|
||||
if not ClassDB.can_instantiate(clazz_name):
|
||||
return GdUnitResult.error("Can't instance Engine class '%s'." % clazz_name)
|
||||
return GdUnitResult.success(ClassDB.instantiate(clazz_name))
|
||||
else:
|
||||
var clazz_path :String = extract_class_path(clazz_name)[0]
|
||||
if not FileAccess.file_exists(clazz_path):
|
||||
return GdUnitResult.error("Class '%s' not found." % clazz_name)
|
||||
var script: GDScript = load(clazz_path)
|
||||
if script != null:
|
||||
return GdUnitResult.success(script.new())
|
||||
else:
|
||||
return GdUnitResult.error("Can't create instance for '%s'." % clazz_name)
|
||||
return GdUnitResult.error("Can't create instance for class '%s'." % str(clazz))
|
||||
|
||||
|
||||
## We do dispose 'GDScriptFunctionState' in a kacky style because the class is not visible anymore
|
||||
static func dispose_function_state(func_state: Variant) -> void:
|
||||
if func_state != null and str(func_state).contains("GDScriptFunctionState"):
|
||||
@warning_ignore("unsafe_method_access")
|
||||
func_state.completed.emit()
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
static func extract_class_path(clazz :Variant) -> PackedStringArray:
|
||||
var clazz_path := PackedStringArray()
|
||||
if clazz is String:
|
||||
@warning_ignore("unsafe_cast")
|
||||
clazz_path.append(clazz as String)
|
||||
return clazz_path
|
||||
if is_instance(clazz):
|
||||
# is instance a script instance?
|
||||
var script: GDScript = clazz.script
|
||||
if script != null:
|
||||
return extract_class_path(script)
|
||||
return clazz_path
|
||||
|
||||
if clazz is GDScript:
|
||||
var script: GDScript = clazz
|
||||
if not script.resource_path.is_empty():
|
||||
clazz_path.append(script.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(script, "_init")
|
||||
var instance: Object = script.callv("new", arg_list)
|
||||
var clazz_info := inst_to_dict(instance)
|
||||
GdUnitTools.free_instance(instance)
|
||||
@warning_ignore("unsafe_cast")
|
||||
clazz_path.append(clazz_info["@path"] as String)
|
||||
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: GDScript = clazz.script
|
||||
if script != null:
|
||||
return extract_class_name(script)
|
||||
@warning_ignore("unsafe_cast")
|
||||
return GdUnitResult.success((clazz as Object).get_class())
|
||||
|
||||
# extract name form full qualified class path
|
||||
if clazz is String:
|
||||
var clazz_name: String = clazz
|
||||
if ClassDB.class_exists(clazz_name):
|
||||
return GdUnitResult.success(clazz_name)
|
||||
var source_script :GDScript = load(clazz_name)
|
||||
clazz_name = GdScriptParser.new().get_class_name(source_script)
|
||||
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):
|
||||
@warning_ignore("unsafe_cast")
|
||||
if (clazz as Script).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
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var instance :Variant = clazz.new()
|
||||
if instance == null:
|
||||
return GdUnitResult.error("Can't create a instance for class '%s'" % str(clazz))
|
||||
var result := extract_class_name(instance)
|
||||
@warning_ignore("return_value_discarded")
|
||||
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 :String in map.keys():
|
||||
var value :Variant = map.get(key)
|
||||
if value is GDScript:
|
||||
var class_path := extract_class_path(value)
|
||||
@warning_ignore("return_value_discarded")
|
||||
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[Dictionary] = func_sig["args"]
|
||||
for arg in args:
|
||||
var value_type :int = arg["type"]
|
||||
var default_value :Variant = 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) -> Variant:
|
||||
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_CALLABLE: return Callable()
|
||||
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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7ldhc4ryfh1v
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
class_name GdUnit4Version
|
||||
extends RefCounted
|
||||
|
||||
const VERSION_PATTERN = "[center][color=#9887c4]gd[/color][color=#7a57d6]Unit[/color][color=#9887c4]4[/color] [color=#9887c4]${version}[/color][/center]"
|
||||
|
||||
var _major :int
|
||||
var _minor :int
|
||||
var _patch :int
|
||||
|
||||
|
||||
func _init(major :int, minor :int, patch :int) -> void:
|
||||
_major = major
|
||||
_minor = minor
|
||||
_patch = patch
|
||||
|
||||
|
||||
static func parse(value :String) -> GdUnit4Version:
|
||||
var regex := RegEx.new()
|
||||
@warning_ignore("return_value_discarded")
|
||||
regex.compile("[a-zA-Z:,-]+")
|
||||
var cleaned := regex.sub(value, "", true)
|
||||
var parts := cleaned.split(".")
|
||||
var major := parts[0].to_int()
|
||||
var minor := parts[1].to_int()
|
||||
var patch := parts[2].to_int() if parts.size() > 2 else 0
|
||||
return GdUnit4Version.new(major, minor, patch)
|
||||
|
||||
|
||||
static func current() -> GdUnit4Version:
|
||||
var config := ConfigFile.new()
|
||||
@warning_ignore("return_value_discarded")
|
||||
config.load('addons/gdUnit4/plugin.cfg')
|
||||
@warning_ignore("unsafe_cast")
|
||||
return parse(config.get_value('plugin', 'version') as String)
|
||||
|
||||
|
||||
func equals(other :GdUnit4Version) -> bool:
|
||||
return _major == other._major and _minor == other._minor and _patch == other._patch
|
||||
|
||||
|
||||
func is_greater(other :GdUnit4Version) -> bool:
|
||||
if _major > other._major:
|
||||
return true
|
||||
if _major == other._major and _minor > other._minor:
|
||||
return true
|
||||
return _major == other._major and _minor == other._minor and _patch > other._patch
|
||||
|
||||
|
||||
static func init_version_label(label :Control) -> void:
|
||||
var config := ConfigFile.new()
|
||||
@warning_ignore("return_value_discarded")
|
||||
config.load('addons/gdUnit4/plugin.cfg')
|
||||
var version :String = config.get_value('plugin', 'version')
|
||||
if label is RichTextLabel:
|
||||
(label as RichTextLabel).text = VERSION_PATTERN.replace('${version}', version)
|
||||
else:
|
||||
(label as Label).text = "gdUnit4 " + version
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "v%d.%d.%d" % [_major, _minor, _patch]
|
||||
|
||||
|
||||
func documentation_version() -> String:
|
||||
return "v%d.%d.x" % [_major, _minor]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbaqjhpbxce3u
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
class_name GdUnitFileAccess
|
||||
extends RefCounted
|
||||
|
||||
const GDUNIT_TEMP := "user://tmp"
|
||||
|
||||
|
||||
static func current_dir() -> String:
|
||||
return ProjectSettings.globalize_path("res://")
|
||||
|
||||
|
||||
static func clear_tmp() -> void:
|
||||
delete_directory(GDUNIT_TEMP)
|
||||
|
||||
|
||||
# Creates a new file under
|
||||
static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess:
|
||||
var file_path := create_temp_dir(relative_path) + "/" + file_name
|
||||
var file := FileAccess.open(file_path, mode)
|
||||
if file == null:
|
||||
push_error("Error creating temporary file at: %s, %s" % [file_path, error_string(FileAccess.get_open_error())])
|
||||
return file
|
||||
|
||||
|
||||
static func temp_dir() -> String:
|
||||
if not DirAccess.dir_exists_absolute(GDUNIT_TEMP):
|
||||
@warning_ignore("return_value_discarded")
|
||||
DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP)
|
||||
return GDUNIT_TEMP
|
||||
|
||||
|
||||
static func create_temp_dir(folder_name :String) -> String:
|
||||
var new_folder := temp_dir() + "/" + folder_name
|
||||
if not DirAccess.dir_exists_absolute(new_folder):
|
||||
@warning_ignore("return_value_discarded")
|
||||
DirAccess.make_dir_recursive_absolute(new_folder)
|
||||
return new_folder
|
||||
|
||||
|
||||
static func copy_file(from_file :String, to_dir :String) -> GdUnitResult:
|
||||
var dir := DirAccess.open(to_dir)
|
||||
if dir != null:
|
||||
var to_file := to_dir + "/" + from_file.get_file()
|
||||
prints("Copy %s to %s" % [from_file, to_file])
|
||||
var error := dir.copy(from_file, to_file)
|
||||
if error != OK:
|
||||
return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_string(error)])
|
||||
return GdUnitResult.success(to_file)
|
||||
return GdUnitResult.error("Directory not found: " + to_dir)
|
||||
|
||||
|
||||
static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool:
|
||||
if not DirAccess.dir_exists_absolute(from_dir):
|
||||
push_error("Source directory not found '%s'" % from_dir)
|
||||
return false
|
||||
|
||||
# check if destination exists
|
||||
if not DirAccess.dir_exists_absolute(to_dir):
|
||||
# create it
|
||||
var err := DirAccess.make_dir_recursive_absolute(to_dir)
|
||||
if err != OK:
|
||||
push_error("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)])
|
||||
return false
|
||||
var source_dir := DirAccess.open(from_dir)
|
||||
var dest_dir := DirAccess.open(to_dir)
|
||||
if source_dir != null:
|
||||
@warning_ignore("return_value_discarded")
|
||||
source_dir.list_dir_begin()
|
||||
var next := "."
|
||||
|
||||
while next != "":
|
||||
next = source_dir.get_next()
|
||||
if next == "" or next == "." or next == "..":
|
||||
continue
|
||||
var source := source_dir.get_current_dir() + "/" + next
|
||||
var dest := dest_dir.get_current_dir() + "/" + next
|
||||
if source_dir.current_is_dir():
|
||||
if recursive:
|
||||
@warning_ignore("return_value_discarded")
|
||||
copy_directory(source + "/", dest, recursive)
|
||||
continue
|
||||
var err := source_dir.copy(source, dest)
|
||||
if err != OK:
|
||||
push_error("Error checked copy file '%s' to '%s'" % [source, dest])
|
||||
return false
|
||||
|
||||
return true
|
||||
else:
|
||||
push_error("Directory not found: " + from_dir)
|
||||
return false
|
||||
|
||||
|
||||
static func delete_directory(path :String, only_content := false) -> void:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir != null:
|
||||
dir.include_hidden = true
|
||||
@warning_ignore("return_value_discarded")
|
||||
dir.list_dir_begin()
|
||||
var file_name := "."
|
||||
while file_name != "":
|
||||
file_name = dir.get_next()
|
||||
if file_name.is_empty() or file_name == "." or file_name == "..":
|
||||
continue
|
||||
var next := path + "/" +file_name
|
||||
if dir.current_is_dir():
|
||||
delete_directory(next)
|
||||
else:
|
||||
# delete file
|
||||
var err := dir.remove(next)
|
||||
if err:
|
||||
push_error("Delete %s failed: %s" % [next, error_string(err)])
|
||||
if not only_content:
|
||||
var err := dir.remove(path)
|
||||
if err:
|
||||
push_error("Delete %s failed: %s" % [path, error_string(err)])
|
||||
|
||||
|
||||
static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
return 0
|
||||
var deleted := 0
|
||||
@warning_ignore("return_value_discarded")
|
||||
dir.list_dir_begin()
|
||||
var next := "."
|
||||
while next != "":
|
||||
next = dir.get_next()
|
||||
if next.is_empty() or next == "." or next == "..":
|
||||
continue
|
||||
if next.begins_with(prefix):
|
||||
var current_index := next.split("_")[1].to_int()
|
||||
if current_index <= index:
|
||||
deleted += 1
|
||||
delete_directory(path + "/" + next)
|
||||
return deleted
|
||||
|
||||
|
||||
# scans given path for sub directories by given prefix and returns the highest index numer
|
||||
# e.g. <prefix_%d>
|
||||
static func find_last_path_index(path :String, prefix :String) -> int:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
return 0
|
||||
var last_iteration := 0
|
||||
@warning_ignore("return_value_discarded")
|
||||
dir.list_dir_begin()
|
||||
var next := "."
|
||||
while next != "":
|
||||
next = dir.get_next()
|
||||
if next.is_empty() or next == "." or next == "..":
|
||||
continue
|
||||
if next.begins_with(prefix):
|
||||
var iteration := next.split("_")[1].to_int()
|
||||
if iteration > last_iteration:
|
||||
last_iteration = iteration
|
||||
return last_iteration
|
||||
|
||||
|
||||
static func as_resource_path(value: String) -> String:
|
||||
if value.begins_with("res://"):
|
||||
return value
|
||||
return "res://" + value.trim_prefix("//").trim_prefix("/").trim_suffix("/")
|
||||
|
||||
|
||||
static func scan_dir(path :String) -> PackedStringArray:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null or not dir.dir_exists(path):
|
||||
return PackedStringArray()
|
||||
var content := PackedStringArray()
|
||||
dir.include_hidden = true
|
||||
@warning_ignore("return_value_discarded")
|
||||
dir.list_dir_begin()
|
||||
var next := "."
|
||||
while next != "":
|
||||
next = dir.get_next()
|
||||
if next.is_empty() or next == "." or next == "..":
|
||||
continue
|
||||
@warning_ignore("return_value_discarded")
|
||||
content.append(next)
|
||||
return content
|
||||
|
||||
|
||||
static func resource_as_array(resource_path :String) -> PackedStringArray:
|
||||
var file := FileAccess.open(resource_path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())])
|
||||
return PackedStringArray()
|
||||
var file_content := PackedStringArray()
|
||||
while not file.eof_reached():
|
||||
@warning_ignore("return_value_discarded")
|
||||
file_content.append(file.get_line())
|
||||
return file_content
|
||||
|
||||
|
||||
static func resource_as_string(resource_path :String) -> String:
|
||||
var file := FileAccess.open(resource_path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())])
|
||||
return ""
|
||||
return file.get_as_text(true)
|
||||
|
||||
|
||||
static func make_qualified_path(path :String) -> String:
|
||||
if path.begins_with("res://"):
|
||||
return path
|
||||
if path.begins_with("//"):
|
||||
return path.replace("//", "res://")
|
||||
if path.begins_with("/"):
|
||||
return "res:/" + path
|
||||
return path
|
||||
|
||||
|
||||
static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult:
|
||||
var zip: ZIPReader = ZIPReader.new()
|
||||
var err := zip.open(zip_package)
|
||||
if err != OK:
|
||||
return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err])
|
||||
var zip_entries: PackedStringArray = zip.get_files()
|
||||
# Get base path and step over archive folder
|
||||
var archive_path := zip_entries[0]
|
||||
zip_entries.remove_at(0)
|
||||
|
||||
for zip_entry in zip_entries:
|
||||
var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "")
|
||||
if zip_entry.ends_with("/"):
|
||||
@warning_ignore("return_value_discarded")
|
||||
DirAccess.make_dir_recursive_absolute(new_file_path)
|
||||
continue
|
||||
var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE)
|
||||
file.store_buffer(zip.read_file(zip_entry))
|
||||
@warning_ignore("return_value_discarded")
|
||||
zip.close()
|
||||
return GdUnitResult.success(dest_path)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dflqb5germp5n
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqndh0nuu8ltx
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cnvq3nb61ei76
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
class_name GdUnitRunnerConfig
|
||||
extends Resource
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
const CONFIG_VERSION = "5.0"
|
||||
const VERSION = "version"
|
||||
const TESTS = "tests"
|
||||
const SERVER_PORT = "server_port"
|
||||
const EXIT_FAIL_FAST = "exit_on_first_fail"
|
||||
|
||||
const CONFIG_FILE = "res://addons/gdUnit4/GdUnitRunner.cfg"
|
||||
|
||||
var _config := {
|
||||
VERSION : CONFIG_VERSION,
|
||||
# a set of directories or testsuite paths as key and a optional set of testcases as values
|
||||
|
||||
TESTS : Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase),
|
||||
|
||||
# the port of running test server for this session
|
||||
SERVER_PORT : -1
|
||||
}
|
||||
|
||||
|
||||
func version() -> String:
|
||||
return _config[VERSION]
|
||||
|
||||
|
||||
func clear() -> GdUnitRunnerConfig:
|
||||
_config[TESTS] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
return self
|
||||
|
||||
|
||||
func set_server_port(port: int) -> GdUnitRunnerConfig:
|
||||
_config[SERVER_PORT] = port
|
||||
return self
|
||||
|
||||
|
||||
func server_port() -> int:
|
||||
return _config.get(SERVER_PORT, -1)
|
||||
|
||||
|
||||
func add_test_cases(tests: Array[GdUnitTestCase]) -> GdUnitRunnerConfig:
|
||||
test_cases().append_array(tests)
|
||||
return self
|
||||
|
||||
|
||||
func test_cases() -> Array[GdUnitTestCase]:
|
||||
return _config.get(TESTS, [])
|
||||
|
||||
|
||||
func save_config(path: String = CONFIG_FILE) -> GdUnitResult:
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
var error := FileAccess.get_open_error()
|
||||
return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, error_string(error)])
|
||||
|
||||
var to_save := {
|
||||
VERSION : CONFIG_VERSION,
|
||||
SERVER_PORT : _config.get(SERVER_PORT),
|
||||
TESTS : Array()
|
||||
}
|
||||
|
||||
var tests: Array = to_save.get(TESTS)
|
||||
for test in test_cases():
|
||||
tests.append(inst_to_dict(test))
|
||||
file.store_string(JSON.stringify(to_save, "\t"))
|
||||
return GdUnitResult.success(path)
|
||||
|
||||
|
||||
func load_config(path: String = CONFIG_FILE) -> GdUnitResult:
|
||||
if not FileAccess.file_exists(path):
|
||||
return GdUnitResult.warn("Can't find test runner configuration '%s'! Please select a test to run." % path)
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
var error := FileAccess.get_open_error()
|
||||
return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, error_string(error)])
|
||||
var content := file.get_as_text()
|
||||
if not content.is_empty() and content[0] == '{':
|
||||
# Parse as json
|
||||
var test_json_conv := JSON.new()
|
||||
var error := test_json_conv.parse(content)
|
||||
if error != OK:
|
||||
return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path)
|
||||
var config: Dictionary = test_json_conv.get_data()
|
||||
if not config.has(VERSION):
|
||||
return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path)
|
||||
|
||||
var default: Array[Dictionary] = Array([], TYPE_DICTIONARY, "", null)
|
||||
var tests_as_json: Array = config.get(TESTS, default)
|
||||
_config = config
|
||||
_config[TESTS] = convert_test_json_to_test_cases(tests_as_json)
|
||||
|
||||
|
||||
fix_value_types()
|
||||
return GdUnitResult.success(path)
|
||||
|
||||
|
||||
func convert_test_json_to_test_cases(jsons: Array) -> Array[GdUnitTestCase]:
|
||||
if jsons.is_empty():
|
||||
return []
|
||||
var tests := jsons.map(func(d: Dictionary) -> GdUnitTestCase:
|
||||
var test: GdUnitTestCase = dict_to_inst(d)
|
||||
# we need o covert manually to the corect type becaus JSON do not handle typed values
|
||||
test.guid = GdUnitGUID.new(str(d["guid"]))
|
||||
test.attribute_index = test.attribute_index as int
|
||||
test.line_number = test.line_number as int
|
||||
return test
|
||||
)
|
||||
return Array(tests, TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
|
||||
|
||||
func fix_value_types() -> void:
|
||||
# fix float value to int json stores all numbers as float
|
||||
var server_port_: int = _config.get(SERVER_PORT, -1)
|
||||
_config[SERVER_PORT] = server_port_
|
||||
|
||||
|
||||
func convert_Array_to_PackedStringArray(data: Dictionary) -> void:
|
||||
for key in data.keys() as Array[String]:
|
||||
var values :Array = data[key]
|
||||
data[key] = PackedStringArray(values)
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return str(_config)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ltvpkh3ayklf
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
# This class provides a runner for scense to simulate interactions like keyboard or mouse
|
||||
class_name GdUnitSceneRunnerImpl
|
||||
extends GdUnitSceneRunner
|
||||
|
||||
|
||||
var GdUnitFuncAssertImpl: GDScript = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE)
|
||||
|
||||
|
||||
# mapping of mouse buttons and his masks
|
||||
const MAP_MOUSE_BUTTON_MASKS := {
|
||||
MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT,
|
||||
MOUSE_BUTTON_RIGHT : MOUSE_BUTTON_MASK_RIGHT,
|
||||
MOUSE_BUTTON_MIDDLE : MOUSE_BUTTON_MASK_MIDDLE,
|
||||
# https://github.com/godotengine/godot/issues/73632
|
||||
MOUSE_BUTTON_WHEEL_UP : 1 << (MOUSE_BUTTON_WHEEL_UP - 1),
|
||||
MOUSE_BUTTON_WHEEL_DOWN : 1 << (MOUSE_BUTTON_WHEEL_DOWN - 1),
|
||||
MOUSE_BUTTON_XBUTTON1 : MOUSE_BUTTON_MASK_MB_XBUTTON1,
|
||||
MOUSE_BUTTON_XBUTTON2 : MOUSE_BUTTON_MASK_MB_XBUTTON2,
|
||||
}
|
||||
|
||||
var _is_disposed := false
|
||||
var _current_scene: Node = null
|
||||
var _awaiter: GdUnitAwaiter = GdUnitAwaiter.new()
|
||||
var _verbose: bool
|
||||
var _simulate_start_time: LocalTime
|
||||
var _last_input_event: InputEvent = null
|
||||
var _mouse_button_on_press := []
|
||||
var _key_on_press := []
|
||||
var _action_on_press := []
|
||||
var _curent_mouse_position: Vector2
|
||||
# holds the touch position for each touch index
|
||||
# { index: int = position: Vector2}
|
||||
var _current_touch_position: Dictionary = {}
|
||||
# holds the curretn touch drag position
|
||||
var _current_touch_drag_position: Vector2 = Vector2.ZERO
|
||||
|
||||
# time factor settings
|
||||
var _time_factor := 1.0
|
||||
var _saved_iterations_per_second: float
|
||||
var _scene_auto_free := false
|
||||
|
||||
|
||||
func _init(p_scene: Variant, p_verbose: bool, p_hide_push_errors := false) -> void:
|
||||
_verbose = p_verbose
|
||||
_saved_iterations_per_second = Engine.get_physics_ticks_per_second()
|
||||
@warning_ignore("return_value_discarded")
|
||||
set_time_factor(1)
|
||||
# handle scene loading by resource path
|
||||
if typeof(p_scene) == TYPE_STRING:
|
||||
@warning_ignore("unsafe_cast")
|
||||
if !ResourceLoader.exists(p_scene as String):
|
||||
if not p_hide_push_errors:
|
||||
push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource does not exists." % p_scene)
|
||||
return
|
||||
if !str(p_scene).ends_with(".tscn") and !str(p_scene).ends_with(".scn") and !str(p_scene).begins_with("uid://"):
|
||||
if not p_hide_push_errors:
|
||||
push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene)
|
||||
return
|
||||
@warning_ignore("unsafe_cast")
|
||||
_current_scene = (load(p_scene as String) as PackedScene).instantiate()
|
||||
_scene_auto_free = true
|
||||
else:
|
||||
# verify we have a node instance
|
||||
if not p_scene is Node:
|
||||
if not p_hide_push_errors:
|
||||
push_error("GdUnitSceneRunner: The given instance '%s' is not a Node." % p_scene)
|
||||
return
|
||||
_current_scene = p_scene
|
||||
if _current_scene == null:
|
||||
if not p_hide_push_errors:
|
||||
push_error("GdUnitSceneRunner: Scene must be not null!")
|
||||
return
|
||||
|
||||
_scene_tree().root.add_child(_current_scene)
|
||||
# do finally reset all open input events when the scene is removed
|
||||
@warning_ignore("return_value_discarded")
|
||||
_scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void:
|
||||
if child == _current_scene:
|
||||
# we need to disable the processing to avoid input flush buffer errors
|
||||
_current_scene.process_mode = Node.PROCESS_MODE_DISABLED
|
||||
_reset_input_to_default()
|
||||
)
|
||||
_simulate_start_time = LocalTime.now()
|
||||
# we need to set inital a valid window otherwise the warp_mouse() is not handled
|
||||
move_window_to_foreground()
|
||||
|
||||
# set inital mouse pos to 0,0
|
||||
var max_iteration_to_wait := 0
|
||||
while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100:
|
||||
Input.warp_mouse(Vector2.ZERO)
|
||||
max_iteration_to_wait += 1
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE and is_instance_valid(self):
|
||||
# reset time factor to normal
|
||||
__deactivate_time_factor()
|
||||
if is_instance_valid(_current_scene):
|
||||
move_window_to_background()
|
||||
_scene_tree().root.remove_child(_current_scene)
|
||||
# do only free scenes instanciated by this runner
|
||||
if _scene_auto_free:
|
||||
_current_scene.free()
|
||||
_is_disposed = true
|
||||
_current_scene = null
|
||||
|
||||
|
||||
func _scene_tree() -> SceneTree:
|
||||
return Engine.get_main_loop() as SceneTree
|
||||
|
||||
|
||||
func await_input_processed() -> void:
|
||||
if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED:
|
||||
Input.flush_buffered_events()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
await (Engine.get_main_loop() as SceneTree).physics_frame
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner:
|
||||
simulate_action_press(action, event_index)
|
||||
simulate_action_release(action, event_index)
|
||||
return self
|
||||
|
||||
|
||||
func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner:
|
||||
__print_current_focus()
|
||||
var event := InputEventAction.new()
|
||||
event.pressed = true
|
||||
event.action = action
|
||||
event.event_index = event_index
|
||||
_action_on_press.append(action)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner:
|
||||
__print_current_focus()
|
||||
var event := InputEventAction.new()
|
||||
event.pressed = false
|
||||
event.action = action
|
||||
event.event_index = event_index
|
||||
_action_on_press.erase(action)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
|
||||
simulate_key_press(key_code, shift_pressed, ctrl_pressed)
|
||||
await _scene_tree().process_frame
|
||||
simulate_key_release(key_code, shift_pressed, ctrl_pressed)
|
||||
return self
|
||||
|
||||
|
||||
func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
|
||||
__print_current_focus()
|
||||
var event := InputEventKey.new()
|
||||
event.pressed = true
|
||||
event.keycode = key_code as Key
|
||||
event.physical_keycode = key_code as Key
|
||||
event.unicode = key_code
|
||||
event.alt_pressed = key_code == KEY_ALT
|
||||
event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
|
||||
event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
|
||||
_apply_input_modifiers(event)
|
||||
_key_on_press.append(key_code)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
|
||||
__print_current_focus()
|
||||
var event := InputEventKey.new()
|
||||
event.pressed = false
|
||||
event.keycode = key_code as Key
|
||||
event.physical_keycode = key_code as Key
|
||||
event.unicode = key_code
|
||||
event.alt_pressed = key_code == KEY_ALT
|
||||
event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
|
||||
event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
|
||||
_apply_input_modifiers(event)
|
||||
_key_on_press.erase(key_code)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
func set_mouse_position(pos: Vector2) -> GdUnitSceneRunner:
|
||||
var event := InputEventMouseMotion.new()
|
||||
event.position = pos
|
||||
event.global_position = get_global_mouse_position()
|
||||
_apply_input_modifiers(event)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
func get_mouse_position() -> Vector2:
|
||||
if _last_input_event is InputEventMouse:
|
||||
return (_last_input_event as InputEventMouse).position
|
||||
var current_scene := scene()
|
||||
if current_scene != null:
|
||||
return current_scene.get_viewport().get_mouse_position()
|
||||
return Vector2.ZERO
|
||||
|
||||
|
||||
func get_global_mouse_position() -> Vector2:
|
||||
return (Engine.get_main_loop() as SceneTree).root.get_mouse_position()
|
||||
|
||||
|
||||
func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner:
|
||||
var event := InputEventMouseMotion.new()
|
||||
event.position = position
|
||||
event.relative = position - get_mouse_position()
|
||||
event.global_position = get_global_mouse_position()
|
||||
_apply_input_mouse_mask(event)
|
||||
_apply_input_modifiers(event)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
||||
var tween := _scene_tree().create_tween()
|
||||
_curent_mouse_position = get_mouse_position()
|
||||
var final_position := _curent_mouse_position + relative
|
||||
tween.tween_property(self, "_curent_mouse_position", final_position, time).set_trans(trans_type)
|
||||
tween.play()
|
||||
|
||||
while not get_mouse_position().is_equal_approx(final_position):
|
||||
simulate_mouse_move(_curent_mouse_position)
|
||||
await _scene_tree().process_frame
|
||||
return self
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
||||
var tween := _scene_tree().create_tween()
|
||||
_curent_mouse_position = get_mouse_position()
|
||||
tween.tween_property(self, "_curent_mouse_position", position, time).set_trans(trans_type)
|
||||
tween.play()
|
||||
|
||||
while not get_mouse_position().is_equal_approx(position):
|
||||
simulate_mouse_move(_curent_mouse_position)
|
||||
await _scene_tree().process_frame
|
||||
return self
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
|
||||
simulate_mouse_button_press(button_index, double_click)
|
||||
simulate_mouse_button_release(button_index)
|
||||
return self
|
||||
|
||||
|
||||
func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = button_index
|
||||
event.pressed = true
|
||||
event.double_click = double_click
|
||||
_apply_input_mouse_position(event)
|
||||
_apply_input_mouse_mask(event)
|
||||
_apply_input_modifiers(event)
|
||||
_mouse_button_on_press.append(button_index)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner:
|
||||
var event := InputEventMouseButton.new()
|
||||
event.button_index = button_index
|
||||
event.pressed = false
|
||||
_apply_input_mouse_position(event)
|
||||
_apply_input_mouse_mask(event)
|
||||
_apply_input_modifiers(event)
|
||||
_mouse_button_on_press.erase(button_index)
|
||||
return _handle_input_event(event)
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
|
||||
simulate_screen_touch_press(index, position, double_tap)
|
||||
simulate_screen_touch_release(index)
|
||||
return self
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
|
||||
if is_emulate_mouse_from_touch():
|
||||
# we need to simulate in addition to the touch the mouse events
|
||||
set_mouse_position(position)
|
||||
simulate_mouse_button_press(MOUSE_BUTTON_LEFT)
|
||||
# push touch press event at position
|
||||
var event := InputEventScreenTouch.new()
|
||||
event.window_id = scene().get_window().get_window_id()
|
||||
event.index = index
|
||||
event.position = position
|
||||
event.double_tap = double_tap
|
||||
event.pressed = true
|
||||
_current_scene.get_viewport().push_input(event)
|
||||
# save current drag position by index
|
||||
_current_touch_position[index] = position
|
||||
return self
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner:
|
||||
if is_emulate_mouse_from_touch():
|
||||
# we need to simulate in addition to the touch the mouse events
|
||||
simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
|
||||
# push touch release event at position
|
||||
var event := InputEventScreenTouch.new()
|
||||
event.window_id = scene().get_window().get_window_id()
|
||||
event.index = index
|
||||
event.position = get_screen_touch_drag_position(index)
|
||||
event.pressed = false
|
||||
event.double_tap = (_last_input_event as InputEventScreenTouch).double_tap if _last_input_event is InputEventScreenTouch else double_tap
|
||||
_current_scene.get_viewport().push_input(event)
|
||||
return self
|
||||
|
||||
|
||||
func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
||||
var current_position: Vector2 = _current_touch_position[index]
|
||||
return await _do_touch_drag_at(index, current_position + relative, time, trans_type)
|
||||
|
||||
|
||||
func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
||||
return await _do_touch_drag_at(index, position, time, trans_type)
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
|
||||
simulate_screen_touch_press(index, position)
|
||||
return await _do_touch_drag_at(index, drop_position, time, trans_type)
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner:
|
||||
if is_emulate_mouse_from_touch():
|
||||
simulate_mouse_move(position)
|
||||
var event := InputEventScreenDrag.new()
|
||||
event.window_id = scene().get_window().get_window_id()
|
||||
event.index = index
|
||||
event.position = position
|
||||
event.relative = _get_screen_touch_drag_position_or_default(index, position) - position
|
||||
event.velocity = event.relative / _scene_tree().root.get_process_delta_time()
|
||||
event.pressure = 1.0
|
||||
_current_touch_position[index] = position
|
||||
_current_scene.get_viewport().push_input(event)
|
||||
return self
|
||||
|
||||
|
||||
func get_screen_touch_drag_position(index: int) -> Vector2:
|
||||
if _current_touch_position.has(index):
|
||||
return _current_touch_position[index]
|
||||
push_error("No touch drag position for index '%d' is set!" % index)
|
||||
return Vector2.ZERO
|
||||
|
||||
|
||||
func is_emulate_mouse_from_touch() -> bool:
|
||||
return ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch", true)
|
||||
|
||||
|
||||
func _get_screen_touch_drag_position_or_default(index: int, default_position: Vector2) -> Vector2:
|
||||
if _current_touch_position.has(index):
|
||||
return _current_touch_position[index]
|
||||
return default_position
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _do_touch_drag_at(index: int, drag_position: Vector2, time: float, trans_type: Tween.TransitionType) -> GdUnitSceneRunner:
|
||||
# start draging
|
||||
var event := InputEventScreenDrag.new()
|
||||
event.window_id = scene().get_window().get_window_id()
|
||||
event.index = index
|
||||
event.position = get_screen_touch_drag_position(index)
|
||||
event.pressure = 1.0
|
||||
_current_touch_drag_position = event.position
|
||||
|
||||
var tween := _scene_tree().create_tween()
|
||||
tween.tween_property(self, "_current_touch_drag_position", drag_position, time).set_trans(trans_type)
|
||||
tween.play()
|
||||
|
||||
while not _current_touch_drag_position.is_equal_approx(drag_position):
|
||||
if is_emulate_mouse_from_touch():
|
||||
# we need to simulate in addition to the drag the mouse move events
|
||||
simulate_mouse_move(event.position)
|
||||
# send touche drag event to new position
|
||||
event.relative = _current_touch_drag_position - event.position
|
||||
event.velocity = event.relative / _scene_tree().root.get_process_delta_time()
|
||||
event.position = _current_touch_drag_position
|
||||
_current_scene.get_viewport().push_input(event)
|
||||
await _scene_tree().process_frame
|
||||
|
||||
# finaly drop it
|
||||
if is_emulate_mouse_from_touch():
|
||||
simulate_mouse_move(drag_position)
|
||||
simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
|
||||
var touch_drop_event := InputEventScreenTouch.new()
|
||||
touch_drop_event.window_id = event.window_id
|
||||
touch_drop_event.index = event.index
|
||||
touch_drop_event.position = drag_position
|
||||
touch_drop_event.pressed = false
|
||||
_current_scene.get_viewport().push_input(touch_drop_event)
|
||||
await _scene_tree().process_frame
|
||||
return self
|
||||
|
||||
|
||||
func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner:
|
||||
_time_factor = min(9.0, time_factor)
|
||||
__activate_time_factor()
|
||||
__print("set time factor: %f" % _time_factor)
|
||||
__print("set physics physics_ticks_per_second: %d" % (_saved_iterations_per_second*_time_factor))
|
||||
return self
|
||||
|
||||
|
||||
func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner:
|
||||
var time_shift_frames :int = max(1, frames / _time_factor)
|
||||
for frame in time_shift_frames:
|
||||
if delta_milli == -1:
|
||||
await _scene_tree().process_frame
|
||||
else:
|
||||
await _scene_tree().create_timer(delta_milli * 0.001).timeout
|
||||
return self
|
||||
|
||||
|
||||
func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner:
|
||||
await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000)
|
||||
return self
|
||||
|
||||
|
||||
func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner:
|
||||
await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000)
|
||||
return self
|
||||
|
||||
|
||||
func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert:
|
||||
return GdUnitFuncAssertImpl.new(scene(), func_name, args)
|
||||
|
||||
|
||||
func await_func_on(instance: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert:
|
||||
return GdUnitFuncAssertImpl.new(instance, func_name, args)
|
||||
|
||||
|
||||
func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void:
|
||||
await _awaiter.await_signal_on(scene(), signal_name, args, timeout)
|
||||
|
||||
|
||||
func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void:
|
||||
await _awaiter.await_signal_on(source, signal_name, args, timeout)
|
||||
|
||||
|
||||
func move_window_to_foreground() -> GdUnitSceneRunner:
|
||||
if not Engine.is_embedded_in_editor():
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||
DisplayServer.window_move_to_foreground()
|
||||
return self
|
||||
|
||||
|
||||
func move_window_to_background() -> GdUnitSceneRunner:
|
||||
if not Engine.is_embedded_in_editor():
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
|
||||
return self
|
||||
|
||||
|
||||
func _property_exists(name: String) -> bool:
|
||||
return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name)
|
||||
|
||||
|
||||
func get_property(name: String) -> Variant:
|
||||
if not _property_exists(name):
|
||||
return "The property '%s' not exist checked loaded scene." % name
|
||||
return scene().get(name)
|
||||
|
||||
|
||||
func set_property(name: String, value: Variant) -> bool:
|
||||
if not _property_exists(name):
|
||||
push_error("The property named '%s' cannot be set, it does not exist!" % name)
|
||||
return false;
|
||||
scene().set(name, value)
|
||||
return true
|
||||
|
||||
|
||||
func invoke(name: String, ...args: Array) -> Variant:
|
||||
if scene().has_method(name):
|
||||
return await scene().callv(name, args)
|
||||
return "The method '%s' not exist checked loaded scene." % name
|
||||
|
||||
|
||||
func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node:
|
||||
return scene().find_child(name, recursive, owned)
|
||||
|
||||
|
||||
func _scene_name() -> String:
|
||||
var scene_script :GDScript = scene().get_script()
|
||||
var scene_name :String = scene().get_name()
|
||||
if not scene_script:
|
||||
return scene_name
|
||||
if not scene_name.begins_with("@"):
|
||||
return scene_name
|
||||
return scene_script.resource_name.get_basename()
|
||||
|
||||
|
||||
func __activate_time_factor() -> void:
|
||||
Engine.set_time_scale(_time_factor)
|
||||
Engine.set_physics_ticks_per_second((_saved_iterations_per_second * _time_factor) as int)
|
||||
|
||||
|
||||
func __deactivate_time_factor() -> void:
|
||||
Engine.set_time_scale(1)
|
||||
Engine.set_physics_ticks_per_second(_saved_iterations_per_second as int)
|
||||
|
||||
|
||||
# copy over current active modifiers
|
||||
func _apply_input_modifiers(event: InputEvent) -> void:
|
||||
if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers:
|
||||
var last_input_event := _last_input_event as InputEventWithModifiers
|
||||
var _event := event as InputEventWithModifiers
|
||||
_event.meta_pressed = _event.meta_pressed or last_input_event.meta_pressed
|
||||
_event.alt_pressed = _event.alt_pressed or last_input_event.alt_pressed
|
||||
_event.shift_pressed = _event.shift_pressed or last_input_event.shift_pressed
|
||||
_event.ctrl_pressed = _event.ctrl_pressed or last_input_event.ctrl_pressed
|
||||
# this line results into reset the control_pressed state!!!
|
||||
#event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap
|
||||
|
||||
|
||||
# copy over current active mouse mask and combine with curren mask
|
||||
func _apply_input_mouse_mask(event: InputEvent) -> void:
|
||||
# first apply last mask
|
||||
if _last_input_event is InputEventMouse and event is InputEventMouse:
|
||||
(event as InputEventMouse).button_mask |= (_last_input_event as InputEventMouse).button_mask
|
||||
if event is InputEventMouseButton:
|
||||
var _event := event as InputEventMouseButton
|
||||
var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(_event.get_button_index(), 0)
|
||||
if _event.is_pressed():
|
||||
_event.button_mask |= button_mask
|
||||
else:
|
||||
_event.button_mask ^= button_mask
|
||||
|
||||
|
||||
# copy over last mouse position if need
|
||||
func _apply_input_mouse_position(event: InputEvent) -> void:
|
||||
if _last_input_event is InputEventMouse and event is InputEventMouseButton:
|
||||
(event as InputEventMouseButton).position = (_last_input_event as InputEventMouse).position
|
||||
|
||||
|
||||
## handle input action via Input modifieres
|
||||
func _handle_actions(event: InputEventAction) -> bool:
|
||||
if not InputMap.event_is_action(event, event.action, true):
|
||||
return false
|
||||
__print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()])
|
||||
if event.is_pressed():
|
||||
Input.action_press(event.action, event.get_strength())
|
||||
else:
|
||||
Input.action_release(event.action)
|
||||
return true
|
||||
|
||||
|
||||
# for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _handle_input_event(event: InputEvent) -> GdUnitSceneRunner:
|
||||
if event is InputEventMouse:
|
||||
Input.warp_mouse((event as InputEventMouse).position as Vector2)
|
||||
Input.parse_input_event(event)
|
||||
|
||||
if event is InputEventAction:
|
||||
_handle_actions(event as InputEventAction)
|
||||
|
||||
var current_scene := scene()
|
||||
if is_instance_valid(current_scene):
|
||||
# do not flush events if node processing disabled otherwise we run into errors at tree removed
|
||||
if _current_scene.process_mode != Node.PROCESS_MODE_DISABLED:
|
||||
Input.flush_buffered_events()
|
||||
__print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()])
|
||||
if(current_scene.has_method("_gui_input")):
|
||||
(current_scene as Control)._gui_input(event)
|
||||
if(current_scene.has_method("_unhandled_input")):
|
||||
current_scene._unhandled_input(event)
|
||||
current_scene.get_viewport().set_input_as_handled()
|
||||
|
||||
# save last input event needs to be merged with next InputEventMouseButton
|
||||
_last_input_event = event
|
||||
return self
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _reset_input_to_default() -> void:
|
||||
# reset all mouse button to inital state if need
|
||||
for m_button :int in _mouse_button_on_press.duplicate():
|
||||
if Input.is_mouse_button_pressed(m_button):
|
||||
simulate_mouse_button_release(m_button)
|
||||
_mouse_button_on_press.clear()
|
||||
|
||||
for key_scancode :int in _key_on_press.duplicate():
|
||||
if Input.is_key_pressed(key_scancode):
|
||||
simulate_key_release(key_scancode)
|
||||
_key_on_press.clear()
|
||||
|
||||
for action :String in _action_on_press.duplicate():
|
||||
if Input.is_action_pressed(action):
|
||||
simulate_action_release(action)
|
||||
_action_on_press.clear()
|
||||
|
||||
if is_instance_valid(_current_scene) and _current_scene.process_mode != Node.PROCESS_MODE_DISABLED:
|
||||
Input.flush_buffered_events()
|
||||
_last_input_event = null
|
||||
|
||||
|
||||
func __print(message: String) -> void:
|
||||
if _verbose:
|
||||
prints(message)
|
||||
|
||||
|
||||
func __print_current_focus() -> void:
|
||||
if not _verbose:
|
||||
return
|
||||
var focused_node := scene().get_viewport().gui_get_focus_owner()
|
||||
if focused_node:
|
||||
prints(" focus checked %s" % focused_node)
|
||||
else:
|
||||
prints(" no focus set")
|
||||
|
||||
|
||||
func scene() -> Node:
|
||||
if is_instance_valid(_current_scene):
|
||||
return _current_scene
|
||||
if not _is_disposed:
|
||||
push_error("The current scene instance is not valid anymore! check your test is valid. e.g. check for missing awaits.")
|
||||
return null
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://7a566a4kfreu
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
@tool
|
||||
class_name GdUnitSettings
|
||||
extends RefCounted
|
||||
|
||||
|
||||
const MAIN_CATEGORY = "gdunit4"
|
||||
# Common Settings
|
||||
const COMMON_SETTINGS = MAIN_CATEGORY + "/settings"
|
||||
|
||||
const GROUP_COMMON = COMMON_SETTINGS + "/common"
|
||||
const UPDATE_NOTIFICATION_ENABLED = GROUP_COMMON + "/update_notification_enabled"
|
||||
const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes"
|
||||
|
||||
const GROUP_HOOKS = MAIN_CATEGORY + "/hooks"
|
||||
const SESSION_HOOKS = GROUP_HOOKS + "/session_hooks"
|
||||
|
||||
const GROUP_TEST = COMMON_SETTINGS + "/test"
|
||||
const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds"
|
||||
const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder"
|
||||
const TEST_SUITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention"
|
||||
const TEST_DISCOVER_ENABLED = GROUP_TEST + "/test_discovery"
|
||||
const TEST_FLAKY_CHECK = GROUP_TEST + "/flaky_check_enable"
|
||||
const TEST_FLAKY_MAX_RETRIES = GROUP_TEST + "/flaky_max_retries"
|
||||
|
||||
|
||||
# Report Setiings
|
||||
const REPORT_SETTINGS = MAIN_CATEGORY + "/report"
|
||||
const GROUP_GODOT = REPORT_SETTINGS + "/godot"
|
||||
const REPORT_PUSH_ERRORS = GROUP_GODOT + "/push_error"
|
||||
const REPORT_SCRIPT_ERRORS = GROUP_GODOT + "/script_error"
|
||||
const REPORT_ORPHANS = REPORT_SETTINGS + "/verbose_orphans"
|
||||
const GROUP_ASSERT = REPORT_SETTINGS + "/assert"
|
||||
const REPORT_ASSERT_WARNINGS = GROUP_ASSERT + "/verbose_warnings"
|
||||
const REPORT_ASSERT_ERRORS = GROUP_ASSERT + "/verbose_errors"
|
||||
const REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE = GROUP_ASSERT + "/strict_number_type_compare"
|
||||
|
||||
# Godot debug stdout/logging settings
|
||||
const CATEGORY_LOGGING := "debug/file_logging/"
|
||||
const STDOUT_ENABLE_TO_FILE = CATEGORY_LOGGING + "enable_file_logging"
|
||||
const STDOUT_WITE_TO_FILE = CATEGORY_LOGGING + "log_path"
|
||||
|
||||
|
||||
# GdUnit Templates
|
||||
const TEMPLATES = MAIN_CATEGORY + "/templates"
|
||||
const TEMPLATES_TS = TEMPLATES + "/testsuite"
|
||||
const TEMPLATE_TS_GD = TEMPLATES_TS + "/GDScript"
|
||||
const TEMPLATE_TS_CS = TEMPLATES_TS + "/CSharpScript"
|
||||
|
||||
|
||||
# UI Setiings
|
||||
const UI_SETTINGS = MAIN_CATEGORY + "/ui"
|
||||
const GROUP_UI_INSPECTOR = UI_SETTINGS + "/inspector"
|
||||
const INSPECTOR_NODE_COLLAPSE = GROUP_UI_INSPECTOR + "/node_collapse"
|
||||
const INSPECTOR_TREE_VIEW_MODE = GROUP_UI_INSPECTOR + "/tree_view_mode"
|
||||
const INSPECTOR_TREE_SORT_MODE = GROUP_UI_INSPECTOR + "/tree_sort_mode"
|
||||
|
||||
|
||||
# Shortcut Setiings
|
||||
const SHORTCUT_SETTINGS = MAIN_CATEGORY + "/Shortcuts"
|
||||
const GROUP_SHORTCUT_INSPECTOR = SHORTCUT_SETTINGS + "/inspector"
|
||||
const SHORTCUT_INSPECTOR_RERUN_TEST = GROUP_SHORTCUT_INSPECTOR + "/rerun_test"
|
||||
const SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG = GROUP_SHORTCUT_INSPECTOR + "/rerun_test_debug"
|
||||
const SHORTCUT_INSPECTOR_RUN_TEST_OVERALL = GROUP_SHORTCUT_INSPECTOR + "/run_test_overall"
|
||||
const SHORTCUT_INSPECTOR_RUN_TEST_STOP = GROUP_SHORTCUT_INSPECTOR + "/run_test_stop"
|
||||
|
||||
const GROUP_SHORTCUT_EDITOR = SHORTCUT_SETTINGS + "/editor"
|
||||
const SHORTCUT_EDITOR_RUN_TEST = GROUP_SHORTCUT_EDITOR + "/run_test"
|
||||
const SHORTCUT_EDITOR_RUN_TEST_DEBUG = GROUP_SHORTCUT_EDITOR + "/run_test_debug"
|
||||
const SHORTCUT_EDITOR_CREATE_TEST = GROUP_SHORTCUT_EDITOR + "/create_test"
|
||||
|
||||
const GROUP_SHORTCUT_FILESYSTEM = SHORTCUT_SETTINGS + "/filesystem"
|
||||
const SHORTCUT_FILESYSTEM_RUN_TEST = GROUP_SHORTCUT_FILESYSTEM + "/run_test"
|
||||
const SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG = GROUP_SHORTCUT_FILESYSTEM + "/run_test_debug"
|
||||
|
||||
|
||||
# Toolbar Setiings
|
||||
const GROUP_UI_TOOLBAR = UI_SETTINGS + "/toolbar"
|
||||
const INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL = GROUP_UI_TOOLBAR + "/run_overall"
|
||||
|
||||
# Feature flags
|
||||
const GROUP_FEATURE = MAIN_CATEGORY + "/feature"
|
||||
|
||||
|
||||
# defaults
|
||||
# server connection timeout in minutes
|
||||
const DEFAULT_SERVER_TIMEOUT :int = 30
|
||||
# test case runtime timeout in seconds
|
||||
const DEFAULT_TEST_TIMEOUT :int = 60*5
|
||||
# the folder to create new test-suites
|
||||
const DEFAULT_TEST_LOOKUP_FOLDER := "test"
|
||||
|
||||
# help texts
|
||||
const HELP_TEST_LOOKUP_FOLDER := "Subfolder where test suites are located (or empty to use source folder directly)"
|
||||
|
||||
enum NAMING_CONVENTIONS {
|
||||
AUTO_DETECT,
|
||||
SNAKE_CASE,
|
||||
PASCAL_CASE,
|
||||
}
|
||||
|
||||
|
||||
const _VALUE_SET_SEPARATOR = "\f" # ASCII Form-feed character (AKA page break)
|
||||
|
||||
|
||||
static func setup() -> void:
|
||||
create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Show notification if new gdUnit4 version is found")
|
||||
# test settings
|
||||
create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Server connection timeout in minutes")
|
||||
create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Test case runtime timeout in seconds")
|
||||
create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER)
|
||||
create_property_if_need(TEST_SUITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Naming convention to use when generating testsuites", NAMING_CONVENTIONS.keys())
|
||||
create_property_if_need(TEST_DISCOVER_ENABLED, false, "Automatically detect new tests in test lookup folders at runtime")
|
||||
create_property_if_need(TEST_FLAKY_CHECK, false, "Rerun tests on failure and mark them as FLAKY")
|
||||
create_property_if_need(TEST_FLAKY_MAX_RETRIES, 3, "Sets the number of retries for rerunning a flaky test")
|
||||
# report settings
|
||||
create_property_if_need(REPORT_PUSH_ERRORS, false, "Report push_error() as failure")
|
||||
create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Report script errors as failure")
|
||||
create_property_if_need(REPORT_ORPHANS, true, "Report orphaned nodes after tests finish")
|
||||
create_property_if_need(REPORT_ASSERT_ERRORS, true, "Report assertion failures as errors")
|
||||
create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Report assertion failures as warnings")
|
||||
create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Compare number values strictly by type (real vs int)")
|
||||
# inspector
|
||||
create_property_if_need(INSPECTOR_NODE_COLLAPSE, true,
|
||||
"Close testsuite node after a successful test run.")
|
||||
create_property_if_need(INSPECTOR_TREE_VIEW_MODE, GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE,
|
||||
"Inspector panel presentation mode", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys())
|
||||
create_property_if_need(INSPECTOR_TREE_SORT_MODE, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED,
|
||||
"Inspector panel sorting mode", GdUnitInspectorTreeConstants.SORT_MODE.keys())
|
||||
create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false,
|
||||
"Show 'Run overall Tests' button in the inspector toolbar")
|
||||
create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Test suite template to use")
|
||||
create_shortcut_properties_if_need()
|
||||
create_property_if_need(SESSION_HOOKS, {} as Dictionary[String,bool])
|
||||
migrate_properties()
|
||||
|
||||
|
||||
static func migrate_properties() -> void:
|
||||
var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder"
|
||||
if get_property(TEST_ROOT_FOLDER) != null:
|
||||
migrate_property(TEST_ROOT_FOLDER,\
|
||||
TEST_LOOKUP_FOLDER,\
|
||||
DEFAULT_TEST_LOOKUP_FOLDER,\
|
||||
HELP_TEST_LOOKUP_FOLDER,\
|
||||
func(value :Variant) -> String: return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value)
|
||||
|
||||
|
||||
static func create_shortcut_properties_if_need() -> void:
|
||||
# inspector
|
||||
create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun the most recently executed tests")
|
||||
create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun the most recently executed tests (Debug mode)")
|
||||
create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug mode)")
|
||||
create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stop the current test execution")
|
||||
# script editor
|
||||
create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Run the currently selected test")
|
||||
create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Run the currently selected test (Debug mode).")
|
||||
create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Create a new test case for the currently selected function")
|
||||
# filesystem
|
||||
create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file")
|
||||
create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file (Debug)")
|
||||
|
||||
|
||||
static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void:
|
||||
if not ProjectSettings.has_setting(name):
|
||||
#prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)])
|
||||
ProjectSettings.set_setting(name, default)
|
||||
|
||||
ProjectSettings.set_initial_value(name, default)
|
||||
help = help if value_set.is_empty() else "%s%s%s" % [help, _VALUE_SET_SEPARATOR, value_set]
|
||||
set_help(name, default, help)
|
||||
|
||||
|
||||
static func set_help(property_name :String, value :Variant, help :String) -> void:
|
||||
ProjectSettings.add_property_info({
|
||||
"name": property_name,
|
||||
"type": typeof(value),
|
||||
"hint": PROPERTY_HINT_TYPE_STRING,
|
||||
"hint_string": help
|
||||
})
|
||||
|
||||
|
||||
static func get_setting(name :String, default :Variant) -> Variant:
|
||||
if ProjectSettings.has_setting(name):
|
||||
return ProjectSettings.get_setting(name)
|
||||
return default
|
||||
|
||||
|
||||
static func is_update_notification_enabled() -> bool:
|
||||
if ProjectSettings.has_setting(UPDATE_NOTIFICATION_ENABLED):
|
||||
return ProjectSettings.get_setting(UPDATE_NOTIFICATION_ENABLED)
|
||||
return false
|
||||
|
||||
|
||||
static func set_update_notification(enable :bool) -> void:
|
||||
ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable)
|
||||
@warning_ignore("return_value_discarded")
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
static func get_log_path() -> String:
|
||||
return ProjectSettings.get_setting(STDOUT_WITE_TO_FILE)
|
||||
|
||||
|
||||
static func set_log_path(path :String) -> void:
|
||||
ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true)
|
||||
ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path)
|
||||
@warning_ignore("return_value_discarded")
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
static func get_session_hooks() -> Dictionary[String, bool]:
|
||||
var property := get_property(SESSION_HOOKS)
|
||||
if property == null:
|
||||
return {}
|
||||
var hooks: Dictionary[String, bool] = property.value()
|
||||
return hooks
|
||||
|
||||
|
||||
static func set_session_hooks(hooks: Dictionary[String, bool]) -> void:
|
||||
var property := get_property(SESSION_HOOKS)
|
||||
property.set_value(hooks)
|
||||
update_property(property)
|
||||
|
||||
|
||||
static func set_inspector_tree_sort_mode(sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void:
|
||||
var property := get_property(INSPECTOR_TREE_SORT_MODE)
|
||||
property.set_value(sort_mode)
|
||||
update_property(property)
|
||||
|
||||
|
||||
static func get_inspector_tree_sort_mode() -> GdUnitInspectorTreeConstants.SORT_MODE:
|
||||
var property := get_property(INSPECTOR_TREE_SORT_MODE)
|
||||
return property.value() if property != null else GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED
|
||||
|
||||
|
||||
static func set_inspector_tree_view_mode(tree_view_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void:
|
||||
var property := get_property(INSPECTOR_TREE_VIEW_MODE)
|
||||
property.set_value(tree_view_mode)
|
||||
update_property(property)
|
||||
|
||||
|
||||
static func get_inspector_tree_view_mode() -> GdUnitInspectorTreeConstants.TREE_VIEW_MODE:
|
||||
var property := get_property(INSPECTOR_TREE_VIEW_MODE)
|
||||
return property.value() if property != null else GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE
|
||||
|
||||
|
||||
# the configured server connection timeout in ms
|
||||
static func server_timeout() -> int:
|
||||
return get_setting(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT) * 60 * 1000
|
||||
|
||||
|
||||
# the configured test case timeout in ms
|
||||
static func test_timeout() -> int:
|
||||
return get_setting(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT) * 1000
|
||||
|
||||
|
||||
# the root folder to store/generate test-suites
|
||||
static func test_root_folder() -> String:
|
||||
return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER)
|
||||
|
||||
|
||||
static func is_verbose_assert_warnings() -> bool:
|
||||
return get_setting(REPORT_ASSERT_WARNINGS, true)
|
||||
|
||||
|
||||
static func is_verbose_assert_errors() -> bool:
|
||||
return get_setting(REPORT_ASSERT_ERRORS, true)
|
||||
|
||||
|
||||
static func is_verbose_orphans() -> bool:
|
||||
return get_setting(REPORT_ORPHANS, true)
|
||||
|
||||
|
||||
static func is_strict_number_type_compare() -> bool:
|
||||
return get_setting(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true)
|
||||
|
||||
|
||||
static func is_report_push_errors() -> bool:
|
||||
return get_setting(REPORT_PUSH_ERRORS, false)
|
||||
|
||||
|
||||
static func is_report_script_errors() -> bool:
|
||||
return get_setting(REPORT_SCRIPT_ERRORS, true)
|
||||
|
||||
|
||||
static func is_inspector_node_collapse() -> bool:
|
||||
return get_setting(INSPECTOR_NODE_COLLAPSE, true)
|
||||
|
||||
|
||||
static func is_inspector_toolbar_button_show() -> bool:
|
||||
return get_setting(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, true)
|
||||
|
||||
|
||||
static func is_test_discover_enabled() -> bool:
|
||||
return get_setting(TEST_DISCOVER_ENABLED, false)
|
||||
|
||||
|
||||
static func is_test_flaky_check_enabled() -> bool:
|
||||
return get_setting(TEST_FLAKY_CHECK, false)
|
||||
|
||||
|
||||
static func is_feature_enabled(feature: String) -> bool:
|
||||
return get_setting(feature, false)
|
||||
|
||||
|
||||
static func get_flaky_max_retries() -> int:
|
||||
return get_setting(TEST_FLAKY_MAX_RETRIES, 3)
|
||||
|
||||
|
||||
static func set_test_discover_enabled(enable :bool) -> void:
|
||||
var property := get_property(TEST_DISCOVER_ENABLED)
|
||||
property.set_value(enable)
|
||||
update_property(property)
|
||||
|
||||
|
||||
static func is_log_enabled() -> bool:
|
||||
return ProjectSettings.get_setting(STDOUT_ENABLE_TO_FILE)
|
||||
|
||||
|
||||
static func list_settings(category: String) -> Array[GdUnitProperty]:
|
||||
var settings: Array[GdUnitProperty] = []
|
||||
for property in ProjectSettings.get_property_list():
|
||||
var property_name :String = property["name"]
|
||||
if property_name.begins_with(category):
|
||||
settings.append(build_property(property_name, property))
|
||||
return settings
|
||||
|
||||
|
||||
static func extract_value_set_from_help(value :String) -> PackedStringArray:
|
||||
var split_value := value.split(_VALUE_SET_SEPARATOR)
|
||||
if not split_value.size() > 1:
|
||||
return PackedStringArray()
|
||||
|
||||
var regex := RegEx.new()
|
||||
@warning_ignore("return_value_discarded")
|
||||
regex.compile("\\[(.+)\\]")
|
||||
var matches := regex.search_all(split_value[1])
|
||||
if matches.is_empty():
|
||||
return PackedStringArray()
|
||||
var values: String = matches[0].get_string(1)
|
||||
return values.replacen(" ", "").replacen("\"", "").split(",", false)
|
||||
|
||||
|
||||
static func extract_help_text(value :String) -> String:
|
||||
return value.split(_VALUE_SET_SEPARATOR)[0]
|
||||
|
||||
|
||||
static func update_property(property :GdUnitProperty) -> Variant:
|
||||
var current_value :Variant = ProjectSettings.get_setting(property.name())
|
||||
if current_value != property.value():
|
||||
var error :Variant = validate_property_value(property)
|
||||
if error != null:
|
||||
return error
|
||||
ProjectSettings.set_setting(property.name(), property.value())
|
||||
GdUnitSignals.instance().gdunit_settings_changed.emit(property)
|
||||
_save_settings()
|
||||
return null
|
||||
|
||||
|
||||
static func reset_property(property :GdUnitProperty) -> void:
|
||||
ProjectSettings.set_setting(property.name(), property.default())
|
||||
GdUnitSignals.instance().gdunit_settings_changed.emit(property)
|
||||
_save_settings()
|
||||
|
||||
|
||||
static func validate_property_value(property :GdUnitProperty) -> Variant:
|
||||
match property.name():
|
||||
TEST_LOOKUP_FOLDER:
|
||||
return validate_lookup_folder(property.value_as_string())
|
||||
_: return null
|
||||
|
||||
|
||||
static func validate_lookup_folder(value :String) -> Variant:
|
||||
if value.is_empty() or value == "/":
|
||||
return null
|
||||
if value.contains("res:"):
|
||||
return "Test Lookup Folder: do not allowed to contains 'res://'"
|
||||
if not value.is_valid_filename():
|
||||
return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)"
|
||||
return null
|
||||
|
||||
|
||||
static func save_property(name :String, value :Variant) -> void:
|
||||
ProjectSettings.set_setting(name, value)
|
||||
_save_settings()
|
||||
|
||||
|
||||
static func _save_settings() -> void:
|
||||
var err := ProjectSettings.save()
|
||||
if err != OK:
|
||||
push_error("Save GdUnit4 settings failed : %s" % error_string(err))
|
||||
return
|
||||
|
||||
|
||||
static func has_property(name :String) -> bool:
|
||||
return ProjectSettings.get_property_list().any(func(property :Dictionary) -> bool: return property["name"] == name)
|
||||
|
||||
|
||||
static func get_property(name :String) -> GdUnitProperty:
|
||||
for property in ProjectSettings.get_property_list():
|
||||
var property_name :String = property["name"]
|
||||
if property_name == name:
|
||||
return build_property(name, property)
|
||||
return null
|
||||
|
||||
|
||||
static func build_property(property_name: String, property: Dictionary) -> GdUnitProperty:
|
||||
var value: Variant = ProjectSettings.get_setting(property_name)
|
||||
var value_type: int = property["type"]
|
||||
var default: Variant = ProjectSettings.property_get_revert(property_name)
|
||||
var help: String = property["hint_string"]
|
||||
var value_set := extract_value_set_from_help(help)
|
||||
return GdUnitProperty.new(property_name, value_type, value, default, extract_help_text(help), value_set)
|
||||
|
||||
|
||||
static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void:
|
||||
var property := get_property(old_property)
|
||||
if property == null:
|
||||
prints("Migration not possible, property '%s' not found" % old_property)
|
||||
return
|
||||
var value :Variant = converter.call(property.value()) if converter.is_valid() else property.value()
|
||||
ProjectSettings.set_setting(new_property, value)
|
||||
ProjectSettings.set_initial_value(new_property, default_value)
|
||||
set_help(new_property, value, help)
|
||||
ProjectSettings.clear(old_property)
|
||||
prints("Successfully migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value])
|
||||
|
||||
|
||||
static func dump_to_tmp() -> void:
|
||||
@warning_ignore("return_value_discarded")
|
||||
ProjectSettings.save_custom("user://project_settings.godot")
|
||||
|
||||
|
||||
static func restore_dump_from_tmp() -> void:
|
||||
@warning_ignore("return_value_discarded")
|
||||
DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://coby4unvmd3eh
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
class_name GdUnitSignalAwaiter
|
||||
extends RefCounted
|
||||
|
||||
signal signal_emitted(action :Variant)
|
||||
|
||||
const NO_ARG :Variant = GdUnitConstants.NO_ARG
|
||||
|
||||
var _wait_on_idle_frame := false
|
||||
var _interrupted := false
|
||||
var _time_left :float = 0
|
||||
var _timeout_millis :int
|
||||
|
||||
|
||||
func _init(timeout_millis :int, wait_on_idle_frame := false) -> void:
|
||||
_timeout_millis = timeout_millis
|
||||
_wait_on_idle_frame = wait_on_idle_frame
|
||||
|
||||
|
||||
func _on_signal_emmited(
|
||||
arg0 :Variant = NO_ARG,
|
||||
arg1 :Variant = NO_ARG,
|
||||
arg2 :Variant = NO_ARG,
|
||||
arg3 :Variant = NO_ARG,
|
||||
arg4 :Variant = NO_ARG,
|
||||
arg5 :Variant = NO_ARG,
|
||||
arg6 :Variant = NO_ARG,
|
||||
arg7 :Variant = NO_ARG,
|
||||
arg8 :Variant = NO_ARG,
|
||||
arg9 :Variant = NO_ARG) -> void:
|
||||
var signal_args :Variant = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
|
||||
signal_emitted.emit(signal_args)
|
||||
|
||||
|
||||
func is_interrupted() -> bool:
|
||||
return _interrupted
|
||||
|
||||
|
||||
func elapsed_time() -> float:
|
||||
return _time_left
|
||||
|
||||
|
||||
func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant:
|
||||
# register checked signal to wait for
|
||||
@warning_ignore("return_value_discarded")
|
||||
source.connect(signal_name, _on_signal_emmited)
|
||||
# install timeout timer
|
||||
var scene_tree := Engine.get_main_loop() as SceneTree
|
||||
var timer := Timer.new()
|
||||
scene_tree.root.add_child(timer)
|
||||
timer.add_to_group("GdUnitTimers")
|
||||
timer.set_one_shot(true)
|
||||
@warning_ignore("return_value_discarded")
|
||||
timer.timeout.connect(_do_interrupt, CONNECT_DEFERRED)
|
||||
timer.start(_timeout_millis * 0.001 * Engine.get_time_scale())
|
||||
|
||||
# holds the emited value
|
||||
var value :Variant
|
||||
# wait for signal is emitted or a timeout is happen
|
||||
while true:
|
||||
value = await signal_emitted
|
||||
if _interrupted:
|
||||
break
|
||||
if not (value is Array):
|
||||
value = [value]
|
||||
if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args):
|
||||
break
|
||||
await scene_tree.process_frame
|
||||
|
||||
source.disconnect(signal_name, _on_signal_emmited)
|
||||
_time_left = timer.time_left
|
||||
timer.queue_free()
|
||||
await scene_tree.process_frame
|
||||
@warning_ignore("unsafe_cast")
|
||||
if value is Array and (value as Array).size() == 1:
|
||||
return value[0]
|
||||
return value
|
||||
|
||||
|
||||
func _do_interrupt() -> void:
|
||||
_interrupted = true
|
||||
signal_emitted.emit(null)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckx5jnr3ip6vp
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# It connects to all signals of given emitter and collects received signals and arguments
|
||||
# The collected signals are cleand finally when the emitter is freed.
|
||||
class_name GdUnitSignalCollector
|
||||
extends RefCounted
|
||||
|
||||
const NO_ARG :Variant = GdUnitConstants.NO_ARG
|
||||
const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"]
|
||||
|
||||
# {
|
||||
# emitter<Object> : {
|
||||
# signal_name<String> : [signal_args<Array>],
|
||||
# ...
|
||||
# }
|
||||
# }
|
||||
var _collected_signals :Dictionary = {}
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
for emitter :Object in _collected_signals.keys():
|
||||
if is_instance_valid(emitter):
|
||||
unregister_emitter(emitter)
|
||||
|
||||
|
||||
# connect to all possible signals defined by the emitter
|
||||
# prepares the signal collection to store received signals and arguments
|
||||
func register_emitter(emitter: Object, force_recreate := false) -> void:
|
||||
if is_instance_valid(emitter):
|
||||
# check emitter is already registerd
|
||||
if _collected_signals.has(emitter):
|
||||
if not force_recreate:
|
||||
return
|
||||
# If the flag recreate is set to true, emitters that are already registered must be deregistered before recreating,
|
||||
# otherwise signals that have already been collected will be evaluated.
|
||||
unregister_emitter(emitter)
|
||||
|
||||
_collected_signals[emitter] = Dictionary()
|
||||
# connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections.
|
||||
if emitter is Node and !(emitter as Node).tree_exiting.is_connected(unregister_emitter):
|
||||
(emitter as Node).tree_exiting.connect(unregister_emitter.bind(emitter))
|
||||
# connect to all signals of the emitter we want to collect
|
||||
for signal_def in emitter.get_signal_list():
|
||||
var signal_name :String = signal_def["name"]
|
||||
# set inital collected to empty
|
||||
if not is_signal_collecting(emitter, signal_name):
|
||||
_collected_signals[emitter][signal_name] = Array()
|
||||
if SIGNAL_BLACK_LIST.find(signal_name) != -1:
|
||||
continue
|
||||
if !emitter.is_connected(signal_name, _on_signal_emmited):
|
||||
var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name))
|
||||
if err != OK:
|
||||
push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)])
|
||||
|
||||
|
||||
# unregister all acquired resources/connections, otherwise it ends up in orphans
|
||||
# is called when the emitter is removed from the parent
|
||||
func unregister_emitter(emitter :Object) -> void:
|
||||
if is_instance_valid(emitter):
|
||||
for signal_def in emitter.get_signal_list():
|
||||
var signal_name :String = signal_def["name"]
|
||||
if emitter.is_connected(signal_name, _on_signal_emmited):
|
||||
emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name))
|
||||
@warning_ignore("return_value_discarded")
|
||||
_collected_signals.erase(emitter)
|
||||
|
||||
|
||||
# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements
|
||||
func _on_signal_emmited(
|
||||
arg0 :Variant= NO_ARG,
|
||||
arg1 :Variant= NO_ARG,
|
||||
arg2 :Variant= NO_ARG,
|
||||
arg3 :Variant= NO_ARG,
|
||||
arg4 :Variant= NO_ARG,
|
||||
arg5 :Variant= NO_ARG,
|
||||
arg6 :Variant= NO_ARG,
|
||||
arg7 :Variant= NO_ARG,
|
||||
arg8 :Variant= NO_ARG,
|
||||
arg9 :Variant= NO_ARG,
|
||||
arg10 :Variant= NO_ARG,
|
||||
arg11 :Variant= NO_ARG) -> void:
|
||||
var signal_args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG)
|
||||
# extract the emitter and signal_name from the last two arguments (see line 61 where is added)
|
||||
var signal_name :String = signal_args.pop_back()
|
||||
var emitter :Object = signal_args.pop_back()
|
||||
#prints("_on_signal_emmited:", emitter, signal_name, signal_args)
|
||||
if is_signal_collecting(emitter, signal_name):
|
||||
@warning_ignore("unsafe_cast")
|
||||
(_collected_signals[emitter][signal_name] as Array).append(signal_args)
|
||||
|
||||
|
||||
func reset_received_signals(emitter: Object, signal_name: String, signal_args: Array) -> void:
|
||||
#_debug_signal_list("before claer");
|
||||
if _collected_signals.has(emitter):
|
||||
var signals_by_emitter :Dictionary = _collected_signals[emitter]
|
||||
if signals_by_emitter.has(signal_name):
|
||||
var received_args: Array = _collected_signals[emitter][signal_name]
|
||||
# We iterate backwarts over to received_args to remove matching args.
|
||||
# This will avoid array corruption see comment on `erase` otherwise we need a timeconsuming duplicate before
|
||||
for arg_pos: int in range(received_args.size()-1, -1, -1):
|
||||
var arg: Variant = received_args[arg_pos]
|
||||
if GdObjects.equals(arg, signal_args):
|
||||
received_args.remove_at(arg_pos)
|
||||
#_debug_signal_list("after claer");
|
||||
|
||||
|
||||
func is_signal_collecting(emitter: Object, signal_name: String) -> bool:
|
||||
@warning_ignore("unsafe_cast")
|
||||
return _collected_signals.has(emitter) and (_collected_signals[emitter] as Dictionary).has(signal_name)
|
||||
|
||||
|
||||
func match(emitter :Object, signal_name :String, args :Array) -> bool:
|
||||
#prints("match", signal_name, _collected_signals[emitter][signal_name]);
|
||||
if _collected_signals.is_empty() or not _collected_signals.has(emitter):
|
||||
return false
|
||||
for received_args :Variant in _collected_signals[emitter][signal_name]:
|
||||
#prints("testing", signal_name, received_args, "vs", args)
|
||||
if GdObjects.equals(received_args, args):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _debug_signal_list(message :String) -> void:
|
||||
prints("-----", message, "-------")
|
||||
prints("senders {")
|
||||
for emitter :Object in _collected_signals:
|
||||
prints("\t", emitter)
|
||||
for signal_name :String in _collected_signals[emitter]:
|
||||
var args :Variant = _collected_signals[emitter][signal_name]
|
||||
prints("\t\t", signal_name, args)
|
||||
prints("}")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cm0rbs8vhdhd1
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
class_name GdUnitSignals
|
||||
extends RefCounted
|
||||
## Singleton class that handles GdUnit's signal communication.[br]
|
||||
## [br]
|
||||
## This class manages all signals used to communicate test events, discovery, and status changes.[br]
|
||||
## It uses a singleton pattern stored in Engine metadata to ensure a single instance.[br]
|
||||
## [br]
|
||||
## Signals are grouped by purpose:[br]
|
||||
## - Client connection handling[br]
|
||||
## - Test execution events[br]
|
||||
## - Test discovery events[br]
|
||||
## - Settings and status updates[br]
|
||||
## [br]
|
||||
## Example usage:[br]
|
||||
## [codeblock]
|
||||
## # Connect to test discovery
|
||||
## GdUnitSignals.instance().gdunit_test_discovered.connect(self._on_test_discovered)
|
||||
##
|
||||
## # Emit test event
|
||||
## GdUnitSignals.instance().gdunit_event.emit(test_event)
|
||||
## [/codeblock]
|
||||
|
||||
|
||||
## Emitted when a client connects to the GdUnit server.[br]
|
||||
## [param client_id] The ID of the connected client.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_client_connected(client_id: int)
|
||||
|
||||
|
||||
## Emitted when a client disconnects from the GdUnit server.[br]
|
||||
## [param client_id] The ID of the disconnected client.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_client_disconnected(client_id: int)
|
||||
|
||||
|
||||
## Emitted when a client terminates unexpectedly.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_client_terminated()
|
||||
|
||||
|
||||
## Emitted when a test execution event occurs.[br]
|
||||
## [param event] The test event containing details about test execution.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_event(event: GdUnitEvent)
|
||||
|
||||
|
||||
## Emitted for test debug events during execution.[br]
|
||||
## [param event] The debug event containing test execution details.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_event_debug(event: GdUnitEvent)
|
||||
|
||||
|
||||
## Emitted to broadcast a general message.[br]
|
||||
## [param message] The message to broadcast.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_message(message: String)
|
||||
|
||||
|
||||
## Emitted to update test failure status.[br]
|
||||
## [param is_failed] Whether the test has failed.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_set_test_failed(is_failed: bool)
|
||||
|
||||
|
||||
## Emitted when a GdUnit setting changes.[br]
|
||||
## [param property] The property that was changed.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_settings_changed(property: GdUnitProperty)
|
||||
|
||||
## Called when a new test case is discovered during the discovery process.
|
||||
## Custom implementations should connect to this signal and store the discovered test case as needed.[br]
|
||||
## [param test_case] The discovered test case instance to be processed.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_test_discover_added(test_case: GdUnitTestCase)
|
||||
|
||||
|
||||
## Emitted when a test case is deleted.[br]
|
||||
## [param test_case] The test case that was deleted.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_test_discover_deleted(test_case: GdUnitTestCase)
|
||||
|
||||
|
||||
## Emitted when a test case is modified.[br]
|
||||
## [param test_case] The test case that was modified.
|
||||
@warning_ignore("unused_signal")
|
||||
signal gdunit_test_discover_modified(test_case: GdUnitTestCase)
|
||||
|
||||
|
||||
const META_KEY := "GdUnitSignals"
|
||||
|
||||
|
||||
## Returns the singleton instance of GdUnitSignals.[br]
|
||||
## Creates a new instance if none exists.[br]
|
||||
## [br]
|
||||
## Returns: The GdUnitSignals singleton instance.
|
||||
static func instance() -> GdUnitSignals:
|
||||
if Engine.has_meta(META_KEY):
|
||||
return Engine.get_meta(META_KEY)
|
||||
var instance_ := GdUnitSignals.new()
|
||||
Engine.set_meta(META_KEY, instance_)
|
||||
return instance_
|
||||
|
||||
|
||||
## Cleans up the singleton instance and disconnects all signals.[br]
|
||||
## [br]
|
||||
## Should be called when GdUnit is shutting down or needs to reset.[br]
|
||||
## Ensures proper cleanup of signal connections and resources.
|
||||
static func dispose() -> void:
|
||||
var signals := instance()
|
||||
# cleanup connected signals
|
||||
for signal_ in signals.get_signal_list():
|
||||
@warning_ignore("unsafe_cast")
|
||||
for connection in signals.get_signal_connection_list(signal_["name"] as StringName):
|
||||
var _signal: Signal = connection["signal"]
|
||||
var _callable: Callable = connection["callable"]
|
||||
_signal.disconnect(_callable)
|
||||
signals = null
|
||||
Engine.remove_meta(META_KEY)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://kj16fg0hf6kn
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
################################################################################
|
||||
# Provides access to a global accessible singleton
|
||||
#
|
||||
# This is a workarount to the existing auto load singleton because of some bugs
|
||||
# around plugin handling
|
||||
################################################################################
|
||||
class_name GdUnitSingleton
|
||||
extends Object
|
||||
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
const MEATA_KEY := "GdUnitSingletons"
|
||||
|
||||
|
||||
static func instance(name: String, clazz: Callable) -> Variant:
|
||||
if Engine.has_meta(name):
|
||||
return Engine.get_meta(name)
|
||||
var singleton: Variant = clazz.call()
|
||||
if is_instance_of(singleton, RefCounted):
|
||||
@warning_ignore("unsafe_cast")
|
||||
push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, (singleton as RefCounted).get_class()])
|
||||
return
|
||||
|
||||
Engine.set_meta(name, singleton)
|
||||
GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton])
|
||||
var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
|
||||
@warning_ignore("return_value_discarded")
|
||||
singletons.append(name)
|
||||
Engine.set_meta(MEATA_KEY, singletons)
|
||||
return singleton
|
||||
|
||||
|
||||
static func unregister(p_singleton: String, use_call_deferred: bool = false) -> void:
|
||||
var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
|
||||
if singletons.has(p_singleton):
|
||||
GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton);
|
||||
var index := singletons.find(p_singleton)
|
||||
singletons.remove_at(index)
|
||||
var instance_: Object = Engine.get_meta(p_singleton)
|
||||
GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_])
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitTools.free_instance(instance_, use_call_deferred)
|
||||
Engine.remove_meta(p_singleton)
|
||||
GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton)
|
||||
Engine.set_meta(MEATA_KEY, singletons)
|
||||
|
||||
|
||||
static func dispose(use_call_deferred: bool = false) -> void:
|
||||
# use a copy because unregister is modify the singletons array
|
||||
var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
|
||||
GdUnitTools.prints_verbose("----------------------------------------------------------------")
|
||||
GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons)
|
||||
for singleton in PackedStringArray(singletons):
|
||||
unregister(singleton, use_call_deferred)
|
||||
Engine.remove_meta(MEATA_KEY)
|
||||
GdUnitTools.prints_verbose("----------------------------------------------------------------")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://4sujouo3vf6d
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ierjyaem56m3
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
class_name GdUnitTestSuiteBuilder
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static func create(source :Script, line_number :int) -> GdUnitResult:
|
||||
var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder())
|
||||
# we need to save and close the testsuite and source if is current opened before modify
|
||||
@warning_ignore("return_value_discarded")
|
||||
ScriptEditorControls.save_an_open_script(source.resource_path)
|
||||
@warning_ignore("return_value_discarded")
|
||||
ScriptEditorControls.save_an_open_script(test_suite_path, true)
|
||||
if source.get_class() == "CSharpScript":
|
||||
return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path)
|
||||
var parser := GdScriptParser.new()
|
||||
var lines := source.source_code.split("\n")
|
||||
var current_line := lines[line_number]
|
||||
var func_name := parser.parse_func_name(current_line)
|
||||
if func_name.is_empty():
|
||||
return GdUnitResult.error("No function found at line: %d." % line_number)
|
||||
return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dthfh16tl5wqc
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bju0nt1bgsc2s
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static var _richtext_normalize: RegEx
|
||||
|
||||
|
||||
static func normalize_text(text :String) -> String:
|
||||
return text.replace("\r", "");
|
||||
|
||||
|
||||
static func richtext_normalize(input :String) -> String:
|
||||
if _richtext_normalize == null:
|
||||
_richtext_normalize = to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]")
|
||||
return _richtext_normalize.sub(input, "", true).replace("\r", "")
|
||||
|
||||
|
||||
static func to_regex(pattern :String) -> RegEx:
|
||||
var regex := RegEx.new()
|
||||
var err := regex.compile(pattern)
|
||||
if err != OK:
|
||||
push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)])
|
||||
return regex
|
||||
|
||||
|
||||
static func prints_verbose(message :String) -> void:
|
||||
if OS.is_stdout_verbose():
|
||||
prints(message)
|
||||
|
||||
|
||||
static func free_instance(instance :Variant, use_call_deferred :bool = false, is_stdout_verbose := false) -> bool:
|
||||
if instance is Array:
|
||||
var as_array: Array = instance
|
||||
for element: Variant in as_array:
|
||||
@warning_ignore("return_value_discarded")
|
||||
free_instance(element)
|
||||
as_array.clear()
|
||||
return true
|
||||
# do not free an already freed instance
|
||||
if not is_instance_valid(instance):
|
||||
return false
|
||||
# do not free a class refernece
|
||||
@warning_ignore("unsafe_cast")
|
||||
if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"):
|
||||
return false
|
||||
if is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():free instance ", instance)
|
||||
@warning_ignore("unsafe_cast")
|
||||
release_double(instance as Object)
|
||||
if instance is RefCounted:
|
||||
@warning_ignore("unsafe_cast")
|
||||
(instance as RefCounted).notification(Object.NOTIFICATION_PREDELETE)
|
||||
# If scene runner freed we explicit await all inputs are processed
|
||||
if instance is GdUnitSceneRunnerImpl:
|
||||
@warning_ignore("unsafe_cast")
|
||||
await (instance as GdUnitSceneRunnerImpl).await_input_processed()
|
||||
return true
|
||||
else:
|
||||
if instance is Timer:
|
||||
var timer: Timer = instance
|
||||
timer.stop()
|
||||
if use_call_deferred:
|
||||
timer.call_deferred("free")
|
||||
else:
|
||||
timer.free()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
return true
|
||||
|
||||
@warning_ignore("unsafe_cast")
|
||||
if instance is Node and (instance as Node).get_parent() != null:
|
||||
var node: Node = instance
|
||||
if is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():remove node from parent ", node.get_parent(), node)
|
||||
if use_call_deferred:
|
||||
node.get_parent().remove_child.call_deferred(node)
|
||||
#instance.call_deferred("set_owner", null)
|
||||
else:
|
||||
node.get_parent().remove_child(node)
|
||||
if is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():freeing `free()` the instance ", instance)
|
||||
if use_call_deferred:
|
||||
@warning_ignore("unsafe_cast")
|
||||
(instance as Object).call_deferred("free")
|
||||
else:
|
||||
@warning_ignore("unsafe_cast")
|
||||
(instance as Object).free()
|
||||
return !is_instance_valid(instance)
|
||||
|
||||
|
||||
static func _release_connections(instance :Object) -> void:
|
||||
if is_instance_valid(instance):
|
||||
# disconnect from all connected signals to force freeing, otherwise it ends up in orphans
|
||||
for connection in instance.get_incoming_connections():
|
||||
var signal_ :Signal = connection["signal"]
|
||||
var callable_ :Callable = connection["callable"]
|
||||
#prints(instance, connection)
|
||||
#prints("signal", signal_.get_name(), signal_.get_object())
|
||||
#prints("callable", callable_.get_object())
|
||||
if instance.has_signal(signal_.get_name()) and instance.is_connected(signal_.get_name(), callable_):
|
||||
#prints("disconnect signal", signal_.get_name(), callable_)
|
||||
instance.disconnect(signal_.get_name(), callable_)
|
||||
release_timers()
|
||||
|
||||
|
||||
static func release_timers() -> void:
|
||||
# we go the new way to hold all gdunit timers in group 'GdUnitTimers'
|
||||
var scene_tree := Engine.get_main_loop() as SceneTree
|
||||
if scene_tree.root == null:
|
||||
return
|
||||
for node :Node in scene_tree.root.get_children():
|
||||
if is_instance_valid(node) and node.is_in_group("GdUnitTimers"):
|
||||
if is_instance_valid(node):
|
||||
scene_tree.root.remove_child.call_deferred(node)
|
||||
(node as Timer).stop()
|
||||
node.queue_free()
|
||||
|
||||
|
||||
# the finally cleaup unfreed resources and singletons
|
||||
static func dispose_all(use_call_deferred :bool = false) -> void:
|
||||
release_timers()
|
||||
GdUnitSingleton.dispose(use_call_deferred)
|
||||
GdUnitSignals.dispose()
|
||||
|
||||
|
||||
# if instance an mock or spy we need manually freeing the self reference
|
||||
static func release_double(instance :Object) -> void:
|
||||
if instance.has_method("__release_double"):
|
||||
instance.call("__release_double")
|
||||
|
||||
|
||||
|
||||
static func find_test_case(test_suite: Node, test_case_name: String, index := -1) -> _TestCase:
|
||||
for test_case: _TestCase in test_suite.get_children():
|
||||
if test_case.test_name() == test_case_name:
|
||||
if index != -1:
|
||||
if test_case._test_case.attribute_index != index:
|
||||
continue
|
||||
return test_case
|
||||
return null
|
||||
|
||||
|
||||
static func register_expect_interupted_by_timeout(test_suite: Node, test_case_name: String) -> void:
|
||||
var test_case := find_test_case(test_suite, test_case_name)
|
||||
if test_case:
|
||||
test_case.expect_to_interupt()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://d05qgv6uu477i
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version
|
||||
class_name GodotVersionFixures
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# handle global_position fixed by https://github.com/godotengine/godot/pull/88473
|
||||
static func set_event_global_position(event: InputEventMouseMotion, global_position: Vector2) -> void:
|
||||
if Engine.get_version_info().hex >= 0x40202 or Engine.get_version_info().hex == 0x40104:
|
||||
event.global_position = event.position
|
||||
else:
|
||||
event.global_position = global_position
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dehxycxsj68ev
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# This class provides Date/Time functionallity to Godot
|
||||
class_name LocalTime
|
||||
extends Resource
|
||||
|
||||
enum TimeUnit {
|
||||
DEFAULT = 0,
|
||||
MILLIS = 1,
|
||||
SECOND = 2,
|
||||
MINUTE = 3,
|
||||
HOUR = 4,
|
||||
DAY = 5,
|
||||
MONTH = 6,
|
||||
YEAR = 7
|
||||
}
|
||||
|
||||
const SECONDS_PER_MINUTE:int = 60
|
||||
const MINUTES_PER_HOUR:int = 60
|
||||
const HOURS_PER_DAY:int = 24
|
||||
const MILLIS_PER_SECOND:int = 1000
|
||||
const MILLIS_PER_MINUTE:int = MILLIS_PER_SECOND * SECONDS_PER_MINUTE
|
||||
const MILLIS_PER_HOUR:int = MILLIS_PER_MINUTE * MINUTES_PER_HOUR
|
||||
|
||||
var _time :int
|
||||
var _hour :int
|
||||
var _minute :int
|
||||
var _second :int
|
||||
var _millisecond :int
|
||||
|
||||
|
||||
static func now() -> LocalTime:
|
||||
return LocalTime.new(_get_system_time_msecs())
|
||||
|
||||
|
||||
static func of_unix_time(time_ms :int) -> LocalTime:
|
||||
return LocalTime.new(time_ms)
|
||||
|
||||
|
||||
static func local_time(hours :int, minutes :int, seconds :int, milliseconds :int) -> LocalTime:
|
||||
return LocalTime.new(MILLIS_PER_HOUR * hours\
|
||||
+ MILLIS_PER_MINUTE * minutes\
|
||||
+ MILLIS_PER_SECOND * seconds\
|
||||
+ milliseconds)
|
||||
|
||||
|
||||
func elapsed_since() -> String:
|
||||
return LocalTime.elapsed(LocalTime._get_system_time_msecs() - _time)
|
||||
|
||||
|
||||
func elapsed_since_ms() -> int:
|
||||
return LocalTime._get_system_time_msecs() - _time
|
||||
|
||||
|
||||
func plus(time_unit :TimeUnit, value :int) -> LocalTime:
|
||||
var addValue:int = 0
|
||||
match time_unit:
|
||||
TimeUnit.MILLIS:
|
||||
addValue = value
|
||||
TimeUnit.SECOND:
|
||||
addValue = value * MILLIS_PER_SECOND
|
||||
TimeUnit.MINUTE:
|
||||
addValue = value * MILLIS_PER_MINUTE
|
||||
TimeUnit.HOUR:
|
||||
addValue = value * MILLIS_PER_HOUR
|
||||
@warning_ignore("return_value_discarded")
|
||||
_init(_time + addValue)
|
||||
return self
|
||||
|
||||
|
||||
static func elapsed(p_time_ms :int) -> String:
|
||||
var local_time_ := LocalTime.new(p_time_ms)
|
||||
if local_time_._hour > 0:
|
||||
return "%dh %dmin %ds %dms" % [local_time_._hour, local_time_._minute, local_time_._second, local_time_._millisecond]
|
||||
if local_time_._minute > 0:
|
||||
return "%dmin %ds %dms" % [local_time_._minute, local_time_._second, local_time_._millisecond]
|
||||
if local_time_._second > 0:
|
||||
return "%ds %dms" % [local_time_._second, local_time_._millisecond]
|
||||
return "%dms" % local_time_._millisecond
|
||||
|
||||
|
||||
# create from epoch timestamp in ms
|
||||
func _init(time: int) -> void:
|
||||
_time = time
|
||||
@warning_ignore("integer_division")
|
||||
_hour = (time / MILLIS_PER_HOUR) % 24
|
||||
@warning_ignore("integer_division")
|
||||
_minute = (time / MILLIS_PER_MINUTE) % 60
|
||||
@warning_ignore("integer_division")
|
||||
_second = (time / MILLIS_PER_SECOND) % 60
|
||||
_millisecond = time % 1000
|
||||
|
||||
|
||||
func hour() -> int:
|
||||
return _hour
|
||||
|
||||
|
||||
func minute() -> int:
|
||||
return _minute
|
||||
|
||||
|
||||
func second() -> int:
|
||||
return _second
|
||||
|
||||
|
||||
func millis() -> int:
|
||||
return _millisecond
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "%02d:%02d:%02d.%03d" % [_hour, _minute, _second, _millisecond]
|
||||
|
||||
|
||||
# wraper to old OS.get_system_time_msecs() function
|
||||
static func _get_system_time_msecs() -> int:
|
||||
return Time.get_unix_time_from_system() * 1000 as int
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmta1h7ndfnko
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
class_name _TestCase
|
||||
extends Node
|
||||
|
||||
signal completed()
|
||||
|
||||
|
||||
var _test_case: GdUnitTestCase
|
||||
var _attribute: TestCaseAttribute
|
||||
var _current_iteration: int = -1
|
||||
var _expect_to_interupt := false
|
||||
var _timer: Timer
|
||||
var _interupted: bool = false
|
||||
var _failed := false
|
||||
var _parameter_set_resolver: GdUnitTestParameterSetResolver
|
||||
var _is_disposed := false
|
||||
var _func_state: Variant
|
||||
|
||||
|
||||
func _init(test_case: GdUnitTestCase, attribute: TestCaseAttribute, fd: GdFunctionDescriptor) -> void:
|
||||
_test_case = test_case
|
||||
_attribute = attribute
|
||||
set_function_descriptor(fd)
|
||||
|
||||
|
||||
func execute(p_test_parameter := Array(), p_iteration := 0) -> void:
|
||||
_failure_received(false)
|
||||
_current_iteration = p_iteration - 1
|
||||
if _current_iteration == - 1:
|
||||
_set_failure_handler()
|
||||
set_timeout()
|
||||
|
||||
if is_parameterized():
|
||||
execute_parameterized()
|
||||
elif not p_test_parameter.is_empty():
|
||||
update_fuzzers(p_test_parameter, p_iteration)
|
||||
_execute_test_case(test_name(), p_test_parameter)
|
||||
else:
|
||||
_execute_test_case(test_name(), [])
|
||||
await completed
|
||||
|
||||
|
||||
func execute_parameterized() -> void:
|
||||
_failure_received(false)
|
||||
set_timeout()
|
||||
|
||||
# Resolve parameter set at runtime to include runtime variables
|
||||
var test_parameters := await _resolve_test_parameters(_test_case.attribute_index)
|
||||
if test_parameters.is_empty():
|
||||
return
|
||||
|
||||
await _execute_test_case(test_name(), test_parameters)
|
||||
|
||||
|
||||
func _resolve_test_parameters(attribute_index: int) -> Array:
|
||||
var result := _parameter_set_resolver.load_parameter_sets(get_parent())
|
||||
if result.is_error():
|
||||
do_skip(true, result.error_message())
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
completed.emit()
|
||||
return []
|
||||
|
||||
# validate the parameter set
|
||||
var parameter_sets: Array = result.value()
|
||||
result = _parameter_set_resolver.validate(parameter_sets, attribute_index)
|
||||
if result.is_error():
|
||||
do_skip(true, result.error_message())
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
completed.emit()
|
||||
return []
|
||||
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var test_parameters: Array = parameter_sets[attribute_index].duplicate()
|
||||
# We need here to add a empty array to override the `test_parameters` to prevent initial "default" parameters from being used.
|
||||
# This prevents objects in the argument list from being unnecessarily re-instantiated.
|
||||
test_parameters.append([])
|
||||
|
||||
return test_parameters
|
||||
|
||||
|
||||
func dispose() -> void:
|
||||
if _is_disposed:
|
||||
return
|
||||
_is_disposed = true
|
||||
Engine.remove_meta("GD_TEST_FAILURE")
|
||||
stop_timer()
|
||||
_remove_failure_handler()
|
||||
_attribute.fuzzers.clear()
|
||||
|
||||
|
||||
@warning_ignore("shadowed_variable_base_class", "redundant_await")
|
||||
func _execute_test_case(name: String, test_parameter: Array) -> void:
|
||||
# save the function state like GDScriptFunctionState to dispose at test timeout to prevent orphan state
|
||||
_func_state = get_parent().callv(name, test_parameter)
|
||||
await _func_state
|
||||
# needs at least on await otherwise it breaks the awaiting chain
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
completed.emit()
|
||||
|
||||
|
||||
func update_fuzzers(input_values: Array, iteration: int) -> void:
|
||||
for fuzzer :Variant in input_values:
|
||||
if fuzzer is Fuzzer:
|
||||
fuzzer._iteration_index = iteration + 1
|
||||
|
||||
|
||||
func set_timeout() -> void:
|
||||
if is_instance_valid(_timer):
|
||||
return
|
||||
var time: float = _attribute.timeout / 1000.0
|
||||
_timer = Timer.new()
|
||||
add_child(_timer)
|
||||
_timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id())
|
||||
@warning_ignore("return_value_discarded")
|
||||
_timer.timeout.connect(do_interrupt, CONNECT_DEFERRED)
|
||||
_timer.set_one_shot(true)
|
||||
_timer.set_wait_time(time)
|
||||
_timer.set_autostart(false)
|
||||
_timer.start()
|
||||
|
||||
|
||||
func do_interrupt() -> void:
|
||||
_interupted = true
|
||||
# We need to dispose manually the function state here
|
||||
GdObjects.dispose_function_state(_func_state)
|
||||
if not is_expect_interupted():
|
||||
var execution_context:= GdUnitThreadManager.get_current_context().get_execution_context()
|
||||
if is_fuzzed():
|
||||
execution_context.add_report(GdUnitReport.new()\
|
||||
.create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout")))
|
||||
else:
|
||||
execution_context.add_report(GdUnitReport.new()\
|
||||
.create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(_attribute.timeout)))
|
||||
completed.emit()
|
||||
|
||||
|
||||
func _set_failure_handler() -> void:
|
||||
if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received):
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received)
|
||||
|
||||
|
||||
func _remove_failure_handler() -> void:
|
||||
if GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received):
|
||||
GdUnitSignals.instance().gdunit_set_test_failed.disconnect(_failure_received)
|
||||
|
||||
|
||||
func _failure_received(is_failed: bool) -> void:
|
||||
# is already failed?
|
||||
if _failed:
|
||||
return
|
||||
_failed = is_failed
|
||||
Engine.set_meta("GD_TEST_FAILURE", is_failed)
|
||||
|
||||
|
||||
func stop_timer() -> void:
|
||||
# finish outstanding timeouts
|
||||
if is_instance_valid(_timer):
|
||||
_timer.stop()
|
||||
_timer.call_deferred("free")
|
||||
_timer = null
|
||||
|
||||
|
||||
func expect_to_interupt() -> void:
|
||||
_expect_to_interupt = true
|
||||
|
||||
|
||||
func is_interupted() -> bool:
|
||||
return _interupted
|
||||
|
||||
|
||||
func is_expect_interupted() -> bool:
|
||||
return _expect_to_interupt
|
||||
|
||||
|
||||
func is_parameterized() -> bool:
|
||||
return _parameter_set_resolver.is_parameterized()
|
||||
|
||||
|
||||
func is_skipped() -> bool:
|
||||
return _attribute.is_skipped
|
||||
|
||||
|
||||
func skip_info() -> String:
|
||||
return _attribute.skip_reason
|
||||
|
||||
|
||||
func id() -> GdUnitGUID:
|
||||
return _test_case.guid
|
||||
|
||||
|
||||
func test_name() -> String:
|
||||
return _test_case.test_name
|
||||
|
||||
|
||||
@warning_ignore("native_method_override")
|
||||
func get_name() -> StringName:
|
||||
return _test_case.test_name
|
||||
|
||||
|
||||
func line_number() -> int:
|
||||
return _test_case.line_number
|
||||
|
||||
|
||||
func iterations() -> int:
|
||||
return _attribute.fuzzer_iterations
|
||||
|
||||
|
||||
func seed_value() -> int:
|
||||
return _attribute.test_seed
|
||||
|
||||
|
||||
func is_fuzzed() -> bool:
|
||||
return not _attribute.fuzzers.is_empty()
|
||||
|
||||
|
||||
func fuzzer_arguments() -> Array[GdFunctionArgument]:
|
||||
return _attribute.fuzzers
|
||||
|
||||
|
||||
func script_path() -> String:
|
||||
return _test_case.source_file
|
||||
|
||||
|
||||
func ResourcePath() -> String:
|
||||
return _test_case.source_file
|
||||
|
||||
|
||||
func generate_seed() -> void:
|
||||
if _attribute.test_seed != -1:
|
||||
seed(_attribute.test_seed)
|
||||
|
||||
|
||||
func do_skip(skipped: bool, reason: String="") -> void:
|
||||
_attribute.is_skipped = skipped
|
||||
_attribute.skip_reason = reason
|
||||
|
||||
|
||||
func set_function_descriptor(fd: GdFunctionDescriptor) -> void:
|
||||
_parameter_set_resolver = GdUnitTestParameterSetResolver.new(fd)
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "%s :%d (%dms)" % [get_name(), _test_case.line_number, _attribute.timeout]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cb2lkpvh0liiv
|
||||
|
||||
BIN
addons/gdUnit4/src/core/assets/touch-button.png
(Stored with Git LFS)
BIN
addons/gdUnit4/src/core/assets/touch-button.png
(Stored with Git LFS)
Binary file not shown.
@@ -2,12 +2,16 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b8nasq23r33s3"
|
||||
valid=false
|
||||
uid="uid://csgvrbao53xmv"
|
||||
path="res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/gdUnit4/src/core/assets/touch-button.png"
|
||||
dest_files=["res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
class_name TestCaseAttribute
|
||||
extends Resource
|
||||
## Holds configuration and metadata for individual test cases.[br]
|
||||
## [br]
|
||||
## This class defines test behaviors and properties such as:[br]
|
||||
## - Test timeouts[br]
|
||||
## - Skip conditions[br]
|
||||
## - Fuzzing parameters[br]
|
||||
## - Random seed values[br]
|
||||
|
||||
|
||||
## When set, no specific timeout value is configured and test will use the [code]test_timeout[/code][br]
|
||||
## value from [GdUnitSettings].
|
||||
const DEFAULT_TIMEOUT := -1
|
||||
|
||||
|
||||
## The maximum time in milliseconds for test completion.[br]
|
||||
## The test fails if execution exceeds this duration.[br]
|
||||
## [br]
|
||||
## When set to [constant DEFAULT_TIMEOUT], uses the value from [method GdUnitSettings.test_timeout].
|
||||
var timeout: int = DEFAULT_TIMEOUT:
|
||||
set(value):
|
||||
timeout = value
|
||||
get:
|
||||
if timeout == DEFAULT_TIMEOUT:
|
||||
# get the default timeout from the settings
|
||||
timeout = GdUnitSettings.test_timeout()
|
||||
return timeout
|
||||
|
||||
|
||||
## The seed used for random number generation in the test.[br]
|
||||
## Ensures reproducible results for randomized test scenarios.[br]
|
||||
## A value of -1 indicates no specific seed is set.
|
||||
var test_seed: int = -1
|
||||
|
||||
|
||||
## Controls whether this test should be skipped during execution.[br]
|
||||
## Useful for temporarily disabling tests without removing them.
|
||||
var is_skipped := false
|
||||
|
||||
|
||||
## Documents why the test is being skipped.[br]
|
||||
## [br]
|
||||
## Should explain the reason for skipping and ideally include:[br]
|
||||
## - Why the test was disabled[br]
|
||||
## - Under what conditions it should be re-enabled[br]
|
||||
## - Any related issues or tickets
|
||||
var skip_reason := "Unknown"
|
||||
|
||||
|
||||
## Number of iterations to run when using fuzzers.[br]
|
||||
## [br]
|
||||
## Fuzzers generate random test data to help find edge cases.[br]
|
||||
## Higher values provide better coverage but increase test duration.
|
||||
var fuzzer_iterations: int = Fuzzer.ITERATION_DEFAULT_COUNT
|
||||
|
||||
|
||||
## Array of fuzzer configurations for test parameters.[br]
|
||||
## [br]
|
||||
## Each [GdFunctionArgument] defines how random test data[br]
|
||||
## should be generated for a particular parameter.
|
||||
var fuzzers: Array[GdFunctionArgument] = []
|
||||
|
||||
|
||||
# There is a bug in `duplicate` see https://github.com/godotengine/godot/issues/98644
|
||||
# we need in addition to overwrite default values with the source values
|
||||
@warning_ignore("native_method_override")
|
||||
func clone() -> Resource:
|
||||
var copy: TestCaseAttribute = TestCaseAttribute.new()
|
||||
copy.timeout = timeout
|
||||
copy.test_seed = test_seed
|
||||
copy.is_skipped = is_skipped
|
||||
copy.skip_reason = skip_reason
|
||||
copy.fuzzer_iterations = fuzzer_iterations
|
||||
copy.fuzzers = fuzzers.duplicate()
|
||||
return copy
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2bres53mgxnw
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
class_name GdUnitCommand
|
||||
extends RefCounted
|
||||
|
||||
|
||||
func _init(p_name :String, p_is_enabled: Callable, p_runnable: Callable, p_shortcut :GdUnitShortcut.ShortCut = GdUnitShortcut.ShortCut.NONE) -> void:
|
||||
assert(p_name != null, "(%s) missing parameter 'name'" % p_name)
|
||||
assert(p_is_enabled != null, "(%s) missing parameter 'is_enabled'" % p_name)
|
||||
assert(p_runnable != null, "(%s) missing parameter 'runnable'" % p_name)
|
||||
assert(p_shortcut != null, "(%s) missing parameter 'shortcut'" % p_name)
|
||||
self.name = p_name
|
||||
self.is_enabled = p_is_enabled
|
||||
self.shortcut = p_shortcut
|
||||
self.runnable = p_runnable
|
||||
|
||||
|
||||
var name: String:
|
||||
set(value):
|
||||
name = value
|
||||
get:
|
||||
return name
|
||||
|
||||
|
||||
var shortcut: GdUnitShortcut.ShortCut:
|
||||
set(value):
|
||||
shortcut = value
|
||||
get:
|
||||
return shortcut
|
||||
|
||||
|
||||
var is_enabled: Callable:
|
||||
set(value):
|
||||
is_enabled = value
|
||||
get:
|
||||
return is_enabled
|
||||
|
||||
|
||||
var runnable: Callable:
|
||||
set(value):
|
||||
runnable = value
|
||||
get:
|
||||
return runnable
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://crmuuvbqy4shs
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
class_name GdUnitCommandHandler
|
||||
extends Object
|
||||
|
||||
signal gdunit_runner_start()
|
||||
signal gdunit_runner_stop(client_id :int)
|
||||
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
const CMD_RUN_OVERALL = "Debug Overall TestSuites"
|
||||
const CMD_RUN_TESTCASE = "Run TestCases"
|
||||
const CMD_RUN_TESTCASE_DEBUG = "Run TestCases (Debug)"
|
||||
const CMD_RUN_TESTSUITE = "Run TestSuites"
|
||||
const CMD_RUN_TESTSUITE_DEBUG = "Run TestSuites (Debug)"
|
||||
const CMD_RERUN_TESTS = "ReRun Tests"
|
||||
const CMD_RERUN_TESTS_DEBUG = "ReRun Tests (Debug)"
|
||||
const CMD_STOP_TEST_RUN = "Stop Test Run"
|
||||
const CMD_CREATE_TESTCASE = "Create TestCase"
|
||||
|
||||
const SETTINGS_SHORTCUT_MAPPING := {
|
||||
"N/A" : GdUnitShortcut.ShortCut.NONE,
|
||||
GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST : GdUnitShortcut.ShortCut.RERUN_TESTS,
|
||||
GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG,
|
||||
GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL : GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL,
|
||||
GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP : GdUnitShortcut.ShortCut.STOP_TEST_RUN,
|
||||
GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE,
|
||||
GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG,
|
||||
GdUnitSettings.SHORTCUT_EDITOR_CREATE_TEST : GdUnitShortcut.ShortCut.CREATE_TEST,
|
||||
GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTSUITE,
|
||||
GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG
|
||||
}
|
||||
|
||||
const CommandMapping := {
|
||||
GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL: GdUnitCommandHandler.CMD_RUN_OVERALL,
|
||||
GdUnitShortcut.ShortCut.RUN_TESTCASE: GdUnitCommandHandler.CMD_RUN_TESTCASE,
|
||||
GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG,
|
||||
GdUnitShortcut.ShortCut.RUN_TESTSUITE: GdUnitCommandHandler.CMD_RUN_TESTSUITE,
|
||||
GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG,
|
||||
GdUnitShortcut.ShortCut.RERUN_TESTS: GdUnitCommandHandler.CMD_RERUN_TESTS,
|
||||
GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG: GdUnitCommandHandler.CMD_RERUN_TESTS_DEBUG,
|
||||
GdUnitShortcut.ShortCut.STOP_TEST_RUN: GdUnitCommandHandler.CMD_STOP_TEST_RUN,
|
||||
GdUnitShortcut.ShortCut.CREATE_TEST: GdUnitCommandHandler.CMD_CREATE_TESTCASE,
|
||||
}
|
||||
|
||||
# the current test runner config
|
||||
var _runner_config := GdUnitRunnerConfig.new()
|
||||
|
||||
# holds the current connected gdUnit runner client id
|
||||
var _client_id: int
|
||||
# if no debug mode we have an process id
|
||||
var _current_runner_process_id: int = 0
|
||||
# hold is current an test running
|
||||
var _is_running: bool = false
|
||||
# holds if the current running tests started in debug mode
|
||||
var _running_debug_mode: bool
|
||||
|
||||
var _commands := {}
|
||||
var _shortcuts := {}
|
||||
|
||||
|
||||
static func instance() -> GdUnitCommandHandler:
|
||||
return GdUnitSingleton.instance("GdUnitCommandHandler", func() -> GdUnitCommandHandler: return GdUnitCommandHandler.new())
|
||||
|
||||
|
||||
@warning_ignore("return_value_discarded")
|
||||
func _init() -> void:
|
||||
assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING)
|
||||
|
||||
GdUnitSignals.instance().gdunit_event.connect(_on_event)
|
||||
GdUnitSignals.instance().gdunit_client_connected.connect(_on_client_connected)
|
||||
GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected)
|
||||
GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
|
||||
# preload previous test execution
|
||||
@warning_ignore("return_value_discarded")
|
||||
_runner_config.load_config()
|
||||
|
||||
init_shortcuts()
|
||||
var is_running := func(_script :Script) -> bool: return _is_running
|
||||
var is_not_running := func(_script :Script) -> bool: return !_is_running
|
||||
register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL))
|
||||
register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE))
|
||||
register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG))
|
||||
register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE, is_not_running, cmd_run_test_suites.bind(false), GdUnitShortcut.ShortCut.RUN_TESTSUITE))
|
||||
register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE_DEBUG, is_not_running, cmd_run_test_suites.bind(true), GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG))
|
||||
register_command(GdUnitCommand.new(CMD_RERUN_TESTS, is_not_running, cmd_run.bind(false), GdUnitShortcut.ShortCut.RERUN_TESTS))
|
||||
register_command(GdUnitCommand.new(CMD_RERUN_TESTS_DEBUG, is_not_running, cmd_run.bind(true), GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG))
|
||||
register_command(GdUnitCommand.new(CMD_CREATE_TESTCASE, is_not_running, cmd_create_test, GdUnitShortcut.ShortCut.CREATE_TEST))
|
||||
register_command(GdUnitCommand.new(CMD_STOP_TEST_RUN, is_running, cmd_stop.bind(_client_id), GdUnitShortcut.ShortCut.STOP_TEST_RUN))
|
||||
|
||||
# schedule discover tests if enabled and running inside the editor
|
||||
if Engine.is_editor_hint() and GdUnitSettings.is_test_discover_enabled():
|
||||
var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(5)
|
||||
@warning_ignore("return_value_discarded")
|
||||
timer.timeout.connect(cmd_discover_tests)
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
_commands.clear()
|
||||
_shortcuts.clear()
|
||||
|
||||
|
||||
func _do_process() -> void:
|
||||
check_test_run_stopped_manually()
|
||||
|
||||
|
||||
# is checking if the user has press the editor stop scene
|
||||
func check_test_run_stopped_manually() -> void:
|
||||
if is_test_running_but_stop_pressed():
|
||||
if GdUnitSettings.is_verbose_assert_warnings():
|
||||
push_warning("Test Runner scene was stopped manually, force stopping the current test run!")
|
||||
cmd_stop(_client_id)
|
||||
|
||||
|
||||
func is_test_running_but_stop_pressed() -> bool:
|
||||
return _running_debug_mode and _is_running and not EditorInterface.is_playing_scene()
|
||||
|
||||
|
||||
func assert_shortcut_mappings(mappings: Dictionary) -> void:
|
||||
for shortcut: int in GdUnitShortcut.ShortCut.values():
|
||||
assert(mappings.values().has(shortcut), "missing settings mapping for shortcut '%s'!" % GdUnitShortcut.ShortCut.keys()[shortcut])
|
||||
|
||||
|
||||
func init_shortcuts() -> void:
|
||||
for shortcut: int in GdUnitShortcut.ShortCut.values():
|
||||
if shortcut == GdUnitShortcut.ShortCut.NONE:
|
||||
continue
|
||||
var property_name: String = SETTINGS_SHORTCUT_MAPPING.find_key(shortcut)
|
||||
var property := GdUnitSettings.get_property(property_name)
|
||||
var keys := GdUnitShortcut.default_keys(shortcut)
|
||||
if property != null:
|
||||
keys = property.value()
|
||||
var inputEvent := create_shortcut_input_even(keys)
|
||||
register_shortcut(shortcut, inputEvent)
|
||||
|
||||
|
||||
func create_shortcut_input_even(key_codes: PackedInt32Array) -> InputEventKey:
|
||||
var inputEvent := InputEventKey.new()
|
||||
inputEvent.pressed = true
|
||||
for key_code in key_codes:
|
||||
match key_code:
|
||||
KEY_ALT:
|
||||
inputEvent.alt_pressed = true
|
||||
KEY_SHIFT:
|
||||
inputEvent.shift_pressed = true
|
||||
KEY_CTRL:
|
||||
inputEvent.ctrl_pressed = true
|
||||
_:
|
||||
inputEvent.keycode = key_code as Key
|
||||
inputEvent.physical_keycode = key_code as Key
|
||||
return inputEvent
|
||||
|
||||
|
||||
func register_shortcut(p_shortcut: GdUnitShortcut.ShortCut, p_input_event: InputEvent) -> void:
|
||||
GdUnitTools.prints_verbose("register shortcut: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[p_shortcut], p_input_event.as_text()])
|
||||
var shortcut := Shortcut.new()
|
||||
shortcut.set_events([p_input_event])
|
||||
var command_name := get_shortcut_command(p_shortcut)
|
||||
_shortcuts[p_shortcut] = GdUnitShortcutAction.new(p_shortcut, shortcut, command_name)
|
||||
|
||||
|
||||
func get_shortcut(shortcut_type: GdUnitShortcut.ShortCut) -> Shortcut:
|
||||
return get_shortcut_action(shortcut_type).shortcut
|
||||
|
||||
|
||||
func get_shortcut_action(shortcut_type: GdUnitShortcut.ShortCut) -> GdUnitShortcutAction:
|
||||
return _shortcuts.get(shortcut_type)
|
||||
|
||||
|
||||
func get_shortcut_command(p_shortcut: GdUnitShortcut.ShortCut) -> String:
|
||||
return CommandMapping.get(p_shortcut, "unknown command")
|
||||
|
||||
|
||||
func register_command(p_command: GdUnitCommand) -> void:
|
||||
_commands[p_command.name] = p_command
|
||||
|
||||
|
||||
func command(cmd_name: String) -> GdUnitCommand:
|
||||
return _commands.get(cmd_name)
|
||||
|
||||
|
||||
func cmd_run_test_suites(scripts: Array[Script], debug: bool, rerun := false) -> void:
|
||||
# Update test discovery
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new())
|
||||
var tests_to_execute: Array[GdUnitTestCase] = []
|
||||
for script in scripts:
|
||||
GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void:
|
||||
tests_to_execute.append(test_case)
|
||||
GdUnitTestDiscoverSink.discover(test_case)
|
||||
)
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0))
|
||||
GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute)
|
||||
|
||||
# create new runner runner_config for fresh run otherwise use saved one
|
||||
if not rerun:
|
||||
var result := _runner_config.clear()\
|
||||
.add_test_cases(tests_to_execute)\
|
||||
.save_config()
|
||||
if result.is_error():
|
||||
push_error(result.error_message())
|
||||
return
|
||||
cmd_run(debug)
|
||||
|
||||
|
||||
func cmd_run_test_case(script: Script, test_case: String, test_param_index: int, debug: bool, rerun := false) -> void:
|
||||
# Update test discovery
|
||||
var tests_to_execute: Array[GdUnitTestCase] = []
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new())
|
||||
GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void:
|
||||
# We filter for a single test
|
||||
if test.test_name == test_case:
|
||||
# We only add selected parameterized test to the execution list
|
||||
if test_param_index == -1:
|
||||
tests_to_execute.append(test)
|
||||
elif test.attribute_index == test_param_index:
|
||||
tests_to_execute.append(test)
|
||||
GdUnitTestDiscoverSink.discover(test)
|
||||
)
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0))
|
||||
GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute)
|
||||
|
||||
# create new runner config for fresh run otherwise use saved one
|
||||
if not rerun:
|
||||
var result := _runner_config.clear()\
|
||||
.add_test_cases(tests_to_execute)\
|
||||
.save_config()
|
||||
if result.is_error():
|
||||
push_error(result.error_message())
|
||||
return
|
||||
cmd_run(debug)
|
||||
|
||||
|
||||
func cmd_run_tests(tests_to_execute: Array[GdUnitTestCase], debug: bool) -> void:
|
||||
# Save tests to runner config before execute
|
||||
var result := _runner_config.clear()\
|
||||
.add_test_cases(tests_to_execute)\
|
||||
.save_config()
|
||||
if result.is_error():
|
||||
push_error(result.error_message())
|
||||
return
|
||||
cmd_run(debug)
|
||||
|
||||
|
||||
func cmd_run_overall(debug: bool) -> void:
|
||||
var tests_to_execute := await GdUnitTestDiscoverer.run()
|
||||
var result := _runner_config.clear()\
|
||||
.add_test_cases(tests_to_execute)\
|
||||
.save_config()
|
||||
if result.is_error():
|
||||
push_error(result.error_message())
|
||||
return
|
||||
cmd_run(debug)
|
||||
|
||||
|
||||
func cmd_run(debug: bool) -> void:
|
||||
# don't start is already running
|
||||
if _is_running:
|
||||
return
|
||||
|
||||
# save current selected excution config
|
||||
var server_port: int = Engine.get_meta("gdunit_server_port")
|
||||
var result := _runner_config.set_server_port(server_port).save_config()
|
||||
if result.is_error():
|
||||
push_error(result.error_message())
|
||||
return
|
||||
# before start we have to save all changes
|
||||
ScriptEditorControls.save_all_open_script()
|
||||
gdunit_runner_start.emit()
|
||||
_current_runner_process_id = -1
|
||||
_running_debug_mode = debug
|
||||
if debug:
|
||||
run_debug_mode()
|
||||
else:
|
||||
run_release_mode()
|
||||
|
||||
|
||||
func cmd_stop(client_id: int) -> void:
|
||||
# don't stop if is already stopped
|
||||
if not _is_running:
|
||||
return
|
||||
_is_running = false
|
||||
gdunit_runner_stop.emit(client_id)
|
||||
if _running_debug_mode:
|
||||
EditorInterface.stop_playing_scene()
|
||||
elif _current_runner_process_id > 0:
|
||||
if OS.is_process_running(_current_runner_process_id):
|
||||
var result := OS.kill(_current_runner_process_id)
|
||||
if result != OK:
|
||||
push_error("ERROR checked stopping GdUnit Test Runner. error code: %s" % result)
|
||||
_current_runner_process_id = -1
|
||||
|
||||
|
||||
func cmd_editor_run_test(debug: bool) -> void:
|
||||
if is_active_script_editor():
|
||||
var cursor_line := active_base_editor().get_caret_line()
|
||||
#run test case?
|
||||
var regex := RegEx.new()
|
||||
@warning_ignore("return_value_discarded")
|
||||
regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)")
|
||||
var result := regex.search(active_base_editor().get_line(cursor_line))
|
||||
if result:
|
||||
var func_name := result.get_string(2).strip_edges()
|
||||
if func_name.begins_with("test_"):
|
||||
cmd_run_test_case(active_script(), func_name, -1, debug)
|
||||
return
|
||||
# otherwise run the full test suite
|
||||
var selected_test_suites: Array[Script] = [active_script()]
|
||||
cmd_run_test_suites(selected_test_suites, debug)
|
||||
|
||||
|
||||
func cmd_create_test() -> void:
|
||||
if not is_active_script_editor():
|
||||
return
|
||||
var cursor_line := active_base_editor().get_caret_line()
|
||||
var result := GdUnitTestSuiteBuilder.create(active_script(), cursor_line)
|
||||
if result.is_error():
|
||||
# show error dialog
|
||||
push_error("Failed to create test case: %s" % result.error_message())
|
||||
return
|
||||
var info: Dictionary = result.value()
|
||||
var script_path: String = info.get("path")
|
||||
var script_line: int = info.get("line")
|
||||
ScriptEditorControls.edit_script(script_path, script_line)
|
||||
|
||||
|
||||
func cmd_discover_tests() -> void:
|
||||
await GdUnitTestDiscoverer.run()
|
||||
|
||||
|
||||
func run_debug_mode() -> void:
|
||||
EditorInterface.play_custom_scene("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn")
|
||||
_is_running = true
|
||||
|
||||
|
||||
func run_release_mode() -> void:
|
||||
var arguments := Array()
|
||||
if OS.is_stdout_verbose():
|
||||
arguments.append("--verbose")
|
||||
arguments.append("--no-window")
|
||||
arguments.append("--path")
|
||||
arguments.append(ProjectSettings.globalize_path("res://"))
|
||||
arguments.append("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn")
|
||||
_current_runner_process_id = OS.create_process(OS.get_executable_path(), arguments, false);
|
||||
_is_running = true
|
||||
|
||||
|
||||
func is_active_script_editor() -> bool:
|
||||
return EditorInterface.get_script_editor().get_current_editor() != null
|
||||
|
||||
|
||||
func active_base_editor() -> TextEdit:
|
||||
return EditorInterface.get_script_editor().get_current_editor().get_base_editor()
|
||||
|
||||
|
||||
func active_script() -> Script:
|
||||
return EditorInterface.get_script_editor().get_current_script()
|
||||
|
||||
|
||||
|
||||
################################################################################
|
||||
# signals handles
|
||||
################################################################################
|
||||
func _on_event(event: GdUnitEvent) -> void:
|
||||
if event.type() == GdUnitEvent.SESSION_CLOSE:
|
||||
cmd_stop(_client_id)
|
||||
|
||||
|
||||
func _on_stop_pressed() -> void:
|
||||
cmd_stop(_client_id)
|
||||
|
||||
|
||||
func _on_run_pressed(debug := false) -> void:
|
||||
cmd_run(debug)
|
||||
|
||||
|
||||
func _on_run_overall_pressed(_debug := false) -> void:
|
||||
cmd_run_overall(true)
|
||||
|
||||
|
||||
func _on_settings_changed(property: GdUnitProperty) -> void:
|
||||
if SETTINGS_SHORTCUT_MAPPING.has(property.name()):
|
||||
var shortcut :GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name())
|
||||
var value: PackedInt32Array = property.value()
|
||||
var input_event := create_shortcut_input_even(value)
|
||||
prints("Shortcut changed: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[shortcut], input_event.as_text()])
|
||||
var action := get_shortcut_action(shortcut)
|
||||
if action != null:
|
||||
action.update_shortcut(input_event)
|
||||
else:
|
||||
register_shortcut(shortcut, input_event)
|
||||
if property.name() == GdUnitSettings.TEST_DISCOVER_ENABLED:
|
||||
var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(3)
|
||||
@warning_ignore("return_value_discarded")
|
||||
timer.timeout.connect(cmd_discover_tests)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Network stuff
|
||||
################################################################################
|
||||
func _on_client_connected(client_id: int) -> void:
|
||||
_client_id = client_id
|
||||
|
||||
|
||||
func _on_client_disconnected(client_id: int) -> void:
|
||||
# only stops is not in debug mode running and the current client
|
||||
if not _running_debug_mode and _client_id == client_id:
|
||||
cmd_stop(client_id)
|
||||
_client_id = -1
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dooc00u4rahqp
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
class_name GdUnitShortcut
|
||||
extends RefCounted
|
||||
|
||||
|
||||
enum ShortCut {
|
||||
NONE,
|
||||
RUN_TESTS_OVERALL,
|
||||
RUN_TESTCASE,
|
||||
RUN_TESTCASE_DEBUG,
|
||||
RUN_TESTSUITE,
|
||||
RUN_TESTSUITE_DEBUG,
|
||||
RERUN_TESTS,
|
||||
RERUN_TESTS_DEBUG,
|
||||
STOP_TEST_RUN,
|
||||
CREATE_TEST,
|
||||
}
|
||||
|
||||
const DEFAULTS_MACOS := {
|
||||
ShortCut.NONE : [],
|
||||
ShortCut.RUN_TESTCASE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5],
|
||||
ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6],
|
||||
ShortCut.RUN_TESTSUITE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5],
|
||||
ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6],
|
||||
ShortCut.RUN_TESTS_OVERALL : [Key.KEY_ALT, Key.KEY_F7],
|
||||
ShortCut.STOP_TEST_RUN : [Key.KEY_ALT, Key.KEY_F8],
|
||||
ShortCut.RERUN_TESTS : [Key.KEY_ALT, Key.KEY_F5],
|
||||
ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_ALT, Key.KEY_F6],
|
||||
ShortCut.CREATE_TEST : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F10],
|
||||
}
|
||||
|
||||
const DEFAULTS_WINDOWS := {
|
||||
ShortCut.NONE : [],
|
||||
ShortCut.RUN_TESTCASE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5],
|
||||
ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6],
|
||||
ShortCut.RUN_TESTSUITE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5],
|
||||
ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6],
|
||||
ShortCut.RUN_TESTS_OVERALL : [Key.KEY_ALT, Key.KEY_F7],
|
||||
ShortCut.STOP_TEST_RUN : [Key.KEY_ALT, Key.KEY_F8],
|
||||
ShortCut.RERUN_TESTS : [Key.KEY_ALT, Key.KEY_F5],
|
||||
ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_ALT, Key.KEY_F6],
|
||||
ShortCut.CREATE_TEST : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F10],
|
||||
}
|
||||
|
||||
|
||||
static func default_keys(shortcut :ShortCut) -> PackedInt32Array:
|
||||
match OS.get_name().to_lower():
|
||||
'windows':
|
||||
return DEFAULTS_WINDOWS[shortcut]
|
||||
'macos':
|
||||
return DEFAULTS_MACOS[shortcut]
|
||||
_:
|
||||
return DEFAULTS_WINDOWS[shortcut]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bsg0clvy7wf0m
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
class_name GdUnitShortcutAction
|
||||
extends RefCounted
|
||||
|
||||
|
||||
func _init(p_type :GdUnitShortcut.ShortCut, p_shortcut :Shortcut, p_command :String) -> void:
|
||||
assert(p_type != null, "missing parameter 'type'")
|
||||
assert(p_shortcut != null, "missing parameter 'shortcut'")
|
||||
assert(p_command != null, "missing parameter 'command'")
|
||||
self.type = p_type
|
||||
self.shortcut = p_shortcut
|
||||
self.command = p_command
|
||||
|
||||
|
||||
var type: GdUnitShortcut.ShortCut:
|
||||
set(value):
|
||||
type = value
|
||||
get:
|
||||
return type
|
||||
|
||||
|
||||
var shortcut: Shortcut:
|
||||
set(value):
|
||||
shortcut = value
|
||||
get:
|
||||
return shortcut
|
||||
|
||||
|
||||
var command: String:
|
||||
set(value):
|
||||
command = value
|
||||
get:
|
||||
return command
|
||||
|
||||
|
||||
func update_shortcut(input_event: InputEventKey) -> void:
|
||||
shortcut.set_events([input_event])
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "GdUnitShortcutAction: %s (%s) -> %s" % [GdUnitShortcut.ShortCut.keys()[type], shortcut.get_as_text(), command]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmlh3hniafm5s
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
## A class representing a globally unique identifier for GdUnit test elements.
|
||||
## Uses random values to generate unique identifiers that can be used
|
||||
## to track and reference test cases and suites across the test framework.
|
||||
class_name GdUnitGUID
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## The internal string representation of the GUID.
|
||||
## Generated using Godot's ResourceUID system when no existing GUID is provided.
|
||||
var _guid: String
|
||||
|
||||
|
||||
## Creates a new GUID instance.
|
||||
## If no GUID is provided, generates a new one using Godot's ResourceUID system.
|
||||
func _init(from_guid: String = "") -> void:
|
||||
if from_guid.is_empty():
|
||||
_guid = _generate_guid()
|
||||
else:
|
||||
_guid = from_guid
|
||||
|
||||
|
||||
## Compares this GUID with another for equality.
|
||||
## Returns true if both GUIDs represent the same unique identifier.
|
||||
func equals(other: GdUnitGUID) -> bool:
|
||||
return other._guid == _guid
|
||||
|
||||
|
||||
## Generates a custom GUID using random bytes.[br]
|
||||
## The format uses 16 random bytes encoded to hex and formatted with hyphens.
|
||||
static func _generate_guid() -> String:
|
||||
# Pre-allocate array with exact size needed
|
||||
var bytes := PackedByteArray()
|
||||
bytes.resize(16)
|
||||
|
||||
# Fill with random bytes
|
||||
for i in range(16):
|
||||
bytes[i] = randi() % 256
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||
|
||||
return bytes.hex_encode().insert(8, "-").insert(16, "-").insert(24, "-")
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return _guid
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://d4lobvde8tufj
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://i4kgxeu6rjiv
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cojycdwxjbkf3
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
## A static utility class that acts as a central sink for test case discovery events in GdUnit4.
|
||||
## Instead of implementing custom sink classes, test discovery consumers should connect to
|
||||
## the GdUnitSignals.gdunit_test_discovered signal to receive test case discoveries.
|
||||
## This design allows for a more flexible and decoupled test discovery system.
|
||||
class_name GdUnitTestDiscoverSink
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## Emits a discovered test case through the GdUnitSignals system.[br]
|
||||
## Sends the test case to all listeners connected to the gdunit_test_discovered signal.[br]
|
||||
## [member test_case] The discovered test case to be broadcast to all connected listeners.
|
||||
static func discover(test_case: GdUnitTestCase) -> void:
|
||||
GdUnitSignals.instance().gdunit_test_discover_added.emit(test_case)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct0kk6824vhxf
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
class_name GdUnitTestDiscoverer
|
||||
extends RefCounted
|
||||
|
||||
|
||||
static func run() -> Array[GdUnitTestCase]:
|
||||
console_log("Running test discovery ..")
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new())
|
||||
|
||||
# We run the test discovery in an extra thread so that the main thread is not blocked
|
||||
var t:= Thread.new()
|
||||
@warning_ignore("return_value_discarded")
|
||||
t.start(func () -> Array[GdUnitTestCase]:
|
||||
# Loading previous test session
|
||||
var runner_config := GdUnitRunnerConfig.new()
|
||||
runner_config.load_config()
|
||||
var recovered_tests := runner_config.test_cases()
|
||||
var test_suite_directories := scan_all_test_directories(GdUnitSettings.test_root_folder())
|
||||
var scanner := GdUnitTestSuiteScanner.new()
|
||||
|
||||
var collected_tests: Array[GdUnitTestCase] = []
|
||||
var collected_test_suites: Array[Script] = []
|
||||
# collect test suites
|
||||
for test_suite_dir in test_suite_directories:
|
||||
collected_test_suites.append_array(scanner.scan_directory(test_suite_dir))
|
||||
|
||||
# Do sync the main thread before emit the discovered test suites to the inspector
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
for test_suites_script in collected_test_suites:
|
||||
discover_tests(test_suites_script, func(test_case: GdUnitTestCase) -> void:
|
||||
# Sync test uid from last test session
|
||||
recover_test_guid(test_case, recovered_tests)
|
||||
collected_tests.append(test_case)
|
||||
GdUnitTestDiscoverSink.discover(test_case)
|
||||
)
|
||||
|
||||
console_log_discover_results(collected_tests)
|
||||
if !recovered_tests.is_empty():
|
||||
console_log("Recovered last test session successfully, %d tests restored." % recovered_tests.size(), true)
|
||||
return collected_tests
|
||||
)
|
||||
# wait unblocked to the tread is finished
|
||||
while t.is_alive():
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
# needs finally to wait for finish
|
||||
var test_to_execute: Array[GdUnitTestCase] = await t.wait_to_finish()
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0))
|
||||
return test_to_execute
|
||||
|
||||
|
||||
## Restores the last test run session by loading the test run config file and rediscover the tests
|
||||
static func restore_last_session() -> void:
|
||||
if GdUnitSettings.is_test_discover_enabled():
|
||||
return
|
||||
|
||||
var runner_config := GdUnitRunnerConfig.new()
|
||||
var result := runner_config.load_config()
|
||||
# Report possible config loading errors
|
||||
if result.is_error():
|
||||
console_log("Recovery of the last test session failed: %s" % result.error_message(), true)
|
||||
# If no config file found, skip test recovery
|
||||
if result.is_warn():
|
||||
return
|
||||
|
||||
# If no tests recorded, skip test recovery
|
||||
var test_cases := runner_config.test_cases()
|
||||
if test_cases.size() == 0:
|
||||
return
|
||||
|
||||
# We run the test session restoring in an extra thread so that the main thread is not blocked
|
||||
var t:= Thread.new()
|
||||
t.start(func () -> void:
|
||||
# Do sync the main thread before emit the discovered test suites to the inspector
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
console_log("Recovering last test session ..", true)
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new())
|
||||
for test_case in test_cases:
|
||||
GdUnitTestDiscoverSink.discover(test_case)
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0))
|
||||
console_log("Recovered last test session successfully, %d tests restored." % test_cases.size(), true)
|
||||
)
|
||||
t.wait_to_finish()
|
||||
|
||||
|
||||
static func recover_test_guid(current: GdUnitTestCase, recovered_tests: Array[GdUnitTestCase]) -> void:
|
||||
for recovered_test in recovered_tests:
|
||||
if recovered_test.fully_qualified_name == current.fully_qualified_name:
|
||||
current.guid = recovered_test.guid
|
||||
|
||||
|
||||
static func console_log_discover_results(tests: Array[GdUnitTestCase]) -> void:
|
||||
var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String:
|
||||
return test.source_file
|
||||
)
|
||||
for suite_tests: Array in grouped_by_suites.values():
|
||||
var test_case: GdUnitTestCase = suite_tests[0]
|
||||
console_log("Discover: TestSuite %s with %d tests found" % [test_case.source_file, suite_tests.size()])
|
||||
console_log("Discover tests done, %d TestSuites and total %d Tests found. " % [grouped_by_suites.size(), tests.size()])
|
||||
console_log("")
|
||||
|
||||
|
||||
static func console_log(message: String, on_console := false) -> void:
|
||||
prints(message)
|
||||
if on_console:
|
||||
GdUnitSignals.instance().gdunit_message.emit(message)
|
||||
|
||||
|
||||
static func filter_tests(method: Dictionary) -> bool:
|
||||
var method_name: String = method["name"]
|
||||
return method_name.begins_with("test_")
|
||||
|
||||
|
||||
static func default_discover_sink(test_case: GdUnitTestCase) -> void:
|
||||
GdUnitTestDiscoverSink.discover(test_case)
|
||||
|
||||
|
||||
static func discover_tests(source_script: Script, discover_sink := default_discover_sink) -> void:
|
||||
if source_script is GDScript:
|
||||
var test_names := source_script.get_script_method_list()\
|
||||
.filter(filter_tests)\
|
||||
.map(func(method: Dictionary) -> String: return method["name"])
|
||||
# no tests discovered?
|
||||
if test_names.is_empty():
|
||||
return
|
||||
|
||||
var parser := GdScriptParser.new()
|
||||
var fds := parser.get_function_descriptors(source_script as GDScript, test_names)
|
||||
for fd in fds:
|
||||
var resolver := GdFunctionParameterSetResolver.new(fd)
|
||||
for test_case in resolver.resolve_test_cases(source_script as GDScript):
|
||||
discover_sink.call(test_case)
|
||||
elif source_script.get_class() == "CSharpScript":
|
||||
if not GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
return
|
||||
for test_case in GdUnit4CSharpApiLoader.discover_tests(source_script):
|
||||
discover_sink.call(test_case)
|
||||
|
||||
|
||||
static func scan_all_test_directories(root: String) -> PackedStringArray:
|
||||
var base_directory := "res://"
|
||||
# If the test root folder is configured as blank, "/", or "res://", use the root folder as described in the settings panel
|
||||
if root.is_empty() or root == "/" or root == base_directory:
|
||||
return [base_directory]
|
||||
return scan_test_directories(base_directory, root, [])
|
||||
|
||||
|
||||
static func scan_test_directories(base_directory: String, test_directory: String, test_suite_paths: PackedStringArray) -> PackedStringArray:
|
||||
print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory])
|
||||
for directory in DirAccess.get_directories_at(base_directory):
|
||||
if directory.begins_with("."):
|
||||
continue
|
||||
var current_directory := normalize_path(base_directory + "/" + directory)
|
||||
if FileAccess.file_exists(current_directory + "/.gdignore"):
|
||||
continue
|
||||
if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory):
|
||||
continue
|
||||
if match_test_directory(directory, test_directory):
|
||||
@warning_ignore("return_value_discarded")
|
||||
test_suite_paths.append(current_directory)
|
||||
else:
|
||||
@warning_ignore("return_value_discarded")
|
||||
scan_test_directories(current_directory, test_directory, test_suite_paths)
|
||||
return test_suite_paths
|
||||
|
||||
|
||||
static func normalize_path(path: String) -> String:
|
||||
return path.replace("///", "//")
|
||||
|
||||
|
||||
static func match_test_directory(directory: String, test_directory: String) -> bool:
|
||||
return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://uakc3vyaaagr
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
class_name GdUnitEvent
|
||||
extends Resource
|
||||
|
||||
const WARNINGS = "warnings"
|
||||
const FAILED = "failed"
|
||||
const FLAKY = "flaky"
|
||||
const ERRORS = "errors"
|
||||
const SKIPPED = "skipped"
|
||||
const ELAPSED_TIME = "elapsed_time"
|
||||
const ORPHAN_NODES = "orphan_nodes"
|
||||
const ERROR_COUNT = "error_count"
|
||||
const FAILED_COUNT = "failed_count"
|
||||
const SKIPPED_COUNT = "skipped_count"
|
||||
const RETRY_COUNT = "retry_count"
|
||||
|
||||
enum {
|
||||
INIT,
|
||||
STOP,
|
||||
TESTSUITE_BEFORE,
|
||||
TESTSUITE_AFTER,
|
||||
TESTCASE_BEFORE,
|
||||
TESTCASE_AFTER,
|
||||
DISCOVER_START,
|
||||
DISCOVER_END,
|
||||
SESSION_START,
|
||||
SESSION_CLOSE
|
||||
}
|
||||
|
||||
var _event_type: int
|
||||
var _guid: GdUnitGUID
|
||||
var _resource_path: String
|
||||
var _suite_name: String
|
||||
var _test_name: String
|
||||
var _total_count: int = 0
|
||||
var _statistics := Dictionary()
|
||||
var _reports: Array[GdUnitReport] = []
|
||||
|
||||
|
||||
func suite_before(p_resource_path: String, p_suite_name: String, p_total_count: int) -> GdUnitEvent:
|
||||
_guid = GdUnitGUID.new()
|
||||
_event_type = TESTSUITE_BEFORE
|
||||
_resource_path = p_resource_path
|
||||
_suite_name = p_suite_name
|
||||
_test_name = "before"
|
||||
_total_count = p_total_count
|
||||
return self
|
||||
|
||||
|
||||
func suite_after(p_resource_path: String, p_suite_name: String, p_statistics: Dictionary = {}, p_reports: Array[GdUnitReport] = []) -> GdUnitEvent:
|
||||
_guid = GdUnitGUID.new()
|
||||
_event_type = TESTSUITE_AFTER
|
||||
_resource_path = p_resource_path
|
||||
_suite_name = p_suite_name
|
||||
_test_name = "after"
|
||||
_statistics = p_statistics
|
||||
_reports = p_reports
|
||||
return self
|
||||
|
||||
|
||||
func test_before(p_guid: GdUnitGUID) -> GdUnitEvent:
|
||||
_event_type = TESTCASE_BEFORE
|
||||
_guid = p_guid
|
||||
return self
|
||||
|
||||
|
||||
func test_after(p_guid: GdUnitGUID, p_statistics: Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent:
|
||||
_event_type = TESTCASE_AFTER
|
||||
_guid = p_guid
|
||||
_statistics = p_statistics
|
||||
_reports = p_reports
|
||||
return self
|
||||
|
||||
|
||||
func type() -> int:
|
||||
return _event_type
|
||||
|
||||
|
||||
func guid() -> GdUnitGUID:
|
||||
return _guid
|
||||
|
||||
|
||||
func suite_name() -> String:
|
||||
return _suite_name
|
||||
|
||||
|
||||
func test_name() -> String:
|
||||
return _test_name
|
||||
|
||||
|
||||
func elapsed_time() -> int:
|
||||
return _statistics.get(ELAPSED_TIME, 0)
|
||||
|
||||
|
||||
func orphan_nodes() -> int:
|
||||
return _statistics.get(ORPHAN_NODES, 0)
|
||||
|
||||
|
||||
func statistic(p_type :String) -> int:
|
||||
return _statistics.get(p_type, 0)
|
||||
|
||||
|
||||
func total_count() -> int:
|
||||
return _total_count
|
||||
|
||||
|
||||
func success_count() -> int:
|
||||
return total_count() - error_count() - failed_count() - skipped_count()
|
||||
|
||||
|
||||
func error_count() -> int:
|
||||
return _statistics.get(ERROR_COUNT, 0)
|
||||
|
||||
|
||||
func failed_count() -> int:
|
||||
return _statistics.get(FAILED_COUNT, 0)
|
||||
|
||||
|
||||
func skipped_count() -> int:
|
||||
return _statistics.get(SKIPPED_COUNT, 0)
|
||||
|
||||
|
||||
func retry_count() -> int:
|
||||
return _statistics.get(RETRY_COUNT, 0)
|
||||
|
||||
|
||||
func resource_path() -> String:
|
||||
return _resource_path
|
||||
|
||||
|
||||
func is_success() -> bool:
|
||||
return not is_failed() and not is_error()
|
||||
|
||||
|
||||
func is_warning() -> bool:
|
||||
return _statistics.get(WARNINGS, false)
|
||||
|
||||
|
||||
func is_failed() -> bool:
|
||||
return _statistics.get(FAILED, false)
|
||||
|
||||
|
||||
func is_error() -> bool:
|
||||
return _statistics.get(ERRORS, false)
|
||||
|
||||
|
||||
func is_flaky() -> bool:
|
||||
return _statistics.get(FLAKY, false)
|
||||
|
||||
|
||||
func is_skipped() -> bool:
|
||||
return _statistics.get(SKIPPED, false)
|
||||
|
||||
|
||||
func reports() -> Array[GdUnitReport]:
|
||||
return _reports
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "Event: %s id:%s %s:%s, %s, %s" % [_event_type, _guid, _suite_name, _test_name, _statistics, _reports]
|
||||
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
var serialized := {
|
||||
"type" : _event_type,
|
||||
"resource_path": _resource_path,
|
||||
"suite_name" : _suite_name,
|
||||
"test_name" : _test_name,
|
||||
"total_count" : _total_count,
|
||||
"statistics" : _statistics
|
||||
}
|
||||
if _guid != null:
|
||||
serialized["guid"] = _guid._guid
|
||||
serialized["reports"] = _serialize_TestReports()
|
||||
return serialized
|
||||
|
||||
|
||||
func deserialize(serialized: Dictionary) -> GdUnitEvent:
|
||||
_event_type = serialized.get("type", null)
|
||||
_guid = GdUnitGUID.new(str(serialized.get("guid", "")))
|
||||
_resource_path = serialized.get("resource_path", null)
|
||||
_suite_name = serialized.get("suite_name", null)
|
||||
_test_name = serialized.get("test_name", "unknown")
|
||||
_total_count = serialized.get("total_count", 0)
|
||||
_statistics = serialized.get("statistics", Dictionary())
|
||||
if serialized.has("reports"):
|
||||
# needs this workaround to copy typed values in the array
|
||||
var reports_to_deserializ :Array[Dictionary] = []
|
||||
@warning_ignore("unsafe_cast")
|
||||
reports_to_deserializ.append_array(serialized.get("reports") as Array)
|
||||
_reports = _deserialize_reports(reports_to_deserializ)
|
||||
return self
|
||||
|
||||
|
||||
func _serialize_TestReports() -> Array[Dictionary]:
|
||||
var serialized_reports :Array[Dictionary] = []
|
||||
for report in _reports:
|
||||
serialized_reports.append(report.serialize())
|
||||
return serialized_reports
|
||||
|
||||
|
||||
func _deserialize_reports(p_reports: Array[Dictionary]) -> Array[GdUnitReport]:
|
||||
var deserialized_reports :Array[GdUnitReport] = []
|
||||
for report in p_reports:
|
||||
var test_report := GdUnitReport.new().deserialize(report)
|
||||
deserialized_reports.append(test_report)
|
||||
return deserialized_reports
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4wkq83n4a4bk
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class_name GdUnitInit
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = INIT
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8t36rmkcsvqm
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class_name GdUnitStop
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = STOP
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cg768i3qgef2x
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
class_name GdUnitEventTestDiscoverEnd
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
var _total_testsuites: int
|
||||
|
||||
|
||||
func _init(testsuite_count: int, test_count: int) -> void:
|
||||
_event_type = DISCOVER_END
|
||||
_total_testsuites = testsuite_count
|
||||
_total_count = test_count
|
||||
|
||||
|
||||
func total_test_suites() -> int:
|
||||
return _total_testsuites
|
||||
|
||||
|
||||
func total_tests() -> int:
|
||||
return _total_count
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bt4blgp4lw3p0
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class_name GdUnitEventTestDiscoverStart
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = DISCOVER_START
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://npuh47e34ud2
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class_name GdUnitSessionClose
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = SESSION_CLOSE
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://eqiw85rg4fgn
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class_name GdUnitSessionStart
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = SESSION_START
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://hpagtimkbhev
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
## The execution context
|
||||
## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor
|
||||
class_name GdUnitExecutionContext
|
||||
|
||||
enum GC_ORPHANS_CHECK {
|
||||
NONE,
|
||||
SUITE_HOOK_AFTER,
|
||||
TEST_HOOK_AFTER,
|
||||
TEST_CASE
|
||||
}
|
||||
|
||||
|
||||
var _parent_context: GdUnitExecutionContext
|
||||
var _sub_context: Array[GdUnitExecutionContext] = []
|
||||
var _orphan_monitor: GdUnitOrphanNodesMonitor
|
||||
var _memory_observer: GdUnitMemoryObserver
|
||||
var _report_collector: GdUnitTestReportCollector
|
||||
var _timer: LocalTime
|
||||
var _test_case_name: StringName
|
||||
var _test_case_parameter_set: Array
|
||||
var _name: String
|
||||
var _test_execution_iteration: int = 0
|
||||
var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled()
|
||||
var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries()
|
||||
var _orphans := -1
|
||||
|
||||
|
||||
var error_monitor: GodotGdErrorMonitor = null:
|
||||
get:
|
||||
if _parent_context != null:
|
||||
return _parent_context.error_monitor
|
||||
if error_monitor == null:
|
||||
error_monitor = GodotGdErrorMonitor.new()
|
||||
return error_monitor
|
||||
|
||||
|
||||
var test_suite: GdUnitTestSuite = null:
|
||||
get:
|
||||
if _parent_context != null:
|
||||
return _parent_context.test_suite
|
||||
return test_suite
|
||||
|
||||
|
||||
var test_case: _TestCase = null:
|
||||
get:
|
||||
if test_case == null and _parent_context != null:
|
||||
return _parent_context.test_case
|
||||
return test_case
|
||||
|
||||
|
||||
func _init(name: StringName, parent_context: GdUnitExecutionContext = null) -> void:
|
||||
_name = name
|
||||
_parent_context = parent_context
|
||||
_timer = LocalTime.now()
|
||||
_orphan_monitor = GdUnitOrphanNodesMonitor.new(name)
|
||||
_orphan_monitor.start()
|
||||
_memory_observer = GdUnitMemoryObserver.new()
|
||||
_report_collector = GdUnitTestReportCollector.new()
|
||||
if parent_context != null:
|
||||
parent_context._sub_context.append(self)
|
||||
|
||||
|
||||
func dispose() -> void:
|
||||
_timer = null
|
||||
_orphan_monitor = null
|
||||
_report_collector = null
|
||||
_memory_observer = null
|
||||
_parent_context = null
|
||||
test_suite = null
|
||||
test_case = null
|
||||
dispose_sub_contexts()
|
||||
|
||||
|
||||
func dispose_sub_contexts() -> void:
|
||||
for context in _sub_context:
|
||||
context.dispose()
|
||||
_sub_context.clear()
|
||||
|
||||
|
||||
static func of(pe: GdUnitExecutionContext) -> GdUnitExecutionContext:
|
||||
var context := GdUnitExecutionContext.new(pe._test_case_name, pe)
|
||||
context._test_case_name = pe._test_case_name
|
||||
context._test_execution_iteration = pe._test_execution_iteration
|
||||
return context
|
||||
|
||||
|
||||
static func of_test_suite(p_test_suite: GdUnitTestSuite) -> GdUnitExecutionContext:
|
||||
assert(p_test_suite, "test_suite is null")
|
||||
var context := GdUnitExecutionContext.new(p_test_suite.get_name())
|
||||
context.test_suite = p_test_suite
|
||||
return context
|
||||
|
||||
|
||||
static func of_test_case(pe: GdUnitExecutionContext, p_test_case: _TestCase) -> GdUnitExecutionContext:
|
||||
assert(p_test_case, "test_case is null")
|
||||
var context := GdUnitExecutionContext.new(p_test_case.get_name(), pe)
|
||||
context.test_case = p_test_case
|
||||
return context
|
||||
|
||||
|
||||
static func of_parameterized_test(pe: GdUnitExecutionContext, test_case_name: String, test_case_parameter_set: Array) -> GdUnitExecutionContext:
|
||||
var context := GdUnitExecutionContext.new(test_case_name, pe)
|
||||
context._test_case_name = test_case_name
|
||||
context._test_case_parameter_set = test_case_parameter_set
|
||||
return context
|
||||
|
||||
|
||||
func get_test_suite_path() -> String:
|
||||
return test_suite.get_script().resource_path
|
||||
|
||||
|
||||
func get_test_suite_name() -> StringName:
|
||||
return test_suite.get_name()
|
||||
|
||||
|
||||
func get_test_case_name() -> StringName:
|
||||
if _test_case_name.is_empty():
|
||||
return test_case._test_case.display_name
|
||||
return _test_case_name
|
||||
|
||||
|
||||
func error_monitor_start() -> void:
|
||||
error_monitor.start()
|
||||
|
||||
|
||||
func error_monitor_stop() -> void:
|
||||
await error_monitor.scan()
|
||||
for error_report in error_monitor.to_reports():
|
||||
if error_report.is_error():
|
||||
_report_collector.push_back(error_report)
|
||||
|
||||
|
||||
func orphan_monitor_start() -> void:
|
||||
_orphan_monitor.start()
|
||||
|
||||
|
||||
func orphan_monitor_stop() -> void:
|
||||
_orphan_monitor.stop()
|
||||
|
||||
|
||||
func add_report(report: GdUnitReport) -> GdUnitReport:
|
||||
_report_collector.push_back(report)
|
||||
return report
|
||||
|
||||
|
||||
func reports() -> Array[GdUnitReport]:
|
||||
return _report_collector.reports()
|
||||
|
||||
|
||||
func collect_reports(recursive: bool) -> Array[GdUnitReport]:
|
||||
if not recursive:
|
||||
return reports()
|
||||
|
||||
# we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended`
|
||||
# we strictly need to copy the reports before adding sub context reports to avoid manipulation of the current context
|
||||
var current_reports := reports().duplicate()
|
||||
for sub_context in _sub_context:
|
||||
current_reports.append_array(sub_context.collect_reports(true))
|
||||
|
||||
return current_reports
|
||||
|
||||
|
||||
func calculate_statistics(reports_: Array[GdUnitReport]) -> Dictionary:
|
||||
var failed_count := GdUnitTestReportCollector.count_failures(reports_)
|
||||
var error_count := GdUnitTestReportCollector.count_errors(reports_)
|
||||
var warn_count := GdUnitTestReportCollector.count_warnings(reports_)
|
||||
var skip_count := GdUnitTestReportCollector.count_skipped(reports_)
|
||||
var is_failed := !is_success()
|
||||
var orphan_count := _count_orphans()
|
||||
var elapsed_time := _timer.elapsed_since_ms()
|
||||
var retries := 1 if _parent_context == null else _sub_context.size()
|
||||
# Mark as flaky if it is successful, but errors were counted
|
||||
var is_flaky := retries > 1 and not is_failed
|
||||
# In the case of a flakiness test, we do not report an error counter, as an unreliable test is considered successful
|
||||
# after a certain number of repetitions.
|
||||
if is_flaky:
|
||||
failed_count = 0
|
||||
|
||||
return {
|
||||
GdUnitEvent.RETRY_COUNT: retries,
|
||||
GdUnitEvent.ELAPSED_TIME: elapsed_time,
|
||||
GdUnitEvent.FAILED: is_failed,
|
||||
GdUnitEvent.ERRORS: error_count > 0,
|
||||
GdUnitEvent.WARNINGS: warn_count > 0,
|
||||
GdUnitEvent.FLAKY: is_flaky,
|
||||
GdUnitEvent.SKIPPED: skip_count > 0,
|
||||
GdUnitEvent.FAILED_COUNT: failed_count,
|
||||
GdUnitEvent.ERROR_COUNT: error_count,
|
||||
GdUnitEvent.SKIPPED_COUNT: skip_count,
|
||||
GdUnitEvent.ORPHAN_NODES: orphan_count,
|
||||
}
|
||||
|
||||
|
||||
func is_success() -> bool:
|
||||
if _sub_context.is_empty():
|
||||
return not _report_collector.has_failures()
|
||||
# we on test suite level?
|
||||
if _parent_context == null:
|
||||
return not _report_collector.has_failures()
|
||||
|
||||
return _sub_context[-1].is_success() and not _report_collector.has_failures()
|
||||
|
||||
|
||||
func is_skipped() -> bool:
|
||||
return (
|
||||
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
|
||||
return c.is_skipped())
|
||||
or test_case.is_skipped() if test_case != null else false
|
||||
)
|
||||
|
||||
|
||||
func is_interupted() -> bool:
|
||||
return false if test_case == null else test_case.is_interupted()
|
||||
|
||||
|
||||
func _count_orphans() -> int:
|
||||
if _orphans != -1:
|
||||
return _orphans
|
||||
|
||||
var orphans := 0
|
||||
for c in _sub_context:
|
||||
if _orphan_monitor.orphan_nodes() != c._orphan_monitor.orphan_nodes():
|
||||
orphans += c._count_orphans()
|
||||
|
||||
_orphans = _orphan_monitor.orphan_nodes()
|
||||
if _orphan_monitor.orphan_nodes() != orphans:
|
||||
_orphans -= orphans
|
||||
|
||||
return _orphans
|
||||
|
||||
|
||||
func sum(accum: int, number: int) -> int:
|
||||
return accum + number
|
||||
|
||||
|
||||
func retry_execution() -> bool:
|
||||
var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries
|
||||
if retry:
|
||||
_test_execution_iteration += 1
|
||||
return retry
|
||||
|
||||
|
||||
func register_auto_free(obj: Variant) -> Variant:
|
||||
return _memory_observer.register_auto_free(obj)
|
||||
|
||||
|
||||
## Runs the gdunit garbage collector to free registered object and handle orphan node reporting
|
||||
func gc(gc_orphan_check: GC_ORPHANS_CHECK = GC_ORPHANS_CHECK.NONE) -> void:
|
||||
# unreference last used assert form the test to prevent memory leaks
|
||||
GdUnitThreadManager.get_current_context().clear_assert()
|
||||
await _memory_observer.gc()
|
||||
orphan_monitor_stop()
|
||||
|
||||
var orphans := _count_orphans()
|
||||
match(gc_orphan_check):
|
||||
GC_ORPHANS_CHECK.SUITE_HOOK_AFTER:
|
||||
if orphans > 0:
|
||||
reports().push_front(GdUnitReport.new() \
|
||||
.create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans)))
|
||||
|
||||
GC_ORPHANS_CHECK.TEST_HOOK_AFTER:
|
||||
if orphans > 0:
|
||||
reports().push_front(GdUnitReport.new()\
|
||||
.create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_test_setup(orphans)))
|
||||
|
||||
GC_ORPHANS_CHECK.TEST_CASE:
|
||||
if orphans > 0:
|
||||
reports().push_front(GdUnitReport.new()\
|
||||
.create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans)))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dm5otinunwsc1
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
## The memory watcher for objects that have been registered and are released when 'gc' is called.
|
||||
class_name GdUnitMemoryObserver
|
||||
extends RefCounted
|
||||
|
||||
const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_"
|
||||
const TAG_AUTO_FREE = "GdUnit4_marked_auto_free"
|
||||
const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
|
||||
var _store :Array[Variant] = []
|
||||
# enable for debugging purposes
|
||||
var _is_stdout_verbose := false
|
||||
const _show_debug := false
|
||||
|
||||
|
||||
## Registration of an instance to be released when an execution phase is completed
|
||||
func register_auto_free(obj :Variant) -> Variant:
|
||||
if not is_instance_valid(obj):
|
||||
return obj
|
||||
# do not register on GDScriptNativeClass
|
||||
@warning_ignore("unsafe_cast")
|
||||
if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") :
|
||||
return obj
|
||||
#if obj is GDScript or obj is ScriptExtension:
|
||||
# return obj
|
||||
if obj is MainLoop:
|
||||
push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj)
|
||||
return
|
||||
if _is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():register auto_free(%s)" % obj)
|
||||
# only register pure objects
|
||||
if obj is GdUnitSceneRunner:
|
||||
_store.push_back(obj)
|
||||
else:
|
||||
_store.append(obj)
|
||||
_tag_object(obj)
|
||||
return obj
|
||||
|
||||
|
||||
# to disable instance guard when run into issues.
|
||||
static func _is_instance_guard_enabled() -> bool:
|
||||
return false
|
||||
|
||||
|
||||
static func debug_observe(name :String, obj :Object, indent :int = 0) -> void:
|
||||
if not _show_debug:
|
||||
return
|
||||
var script :GDScript= obj if obj is GDScript else obj.get_script()
|
||||
if script:
|
||||
var base_script :GDScript = script.get_base_script()
|
||||
@warning_ignore("unsafe_method_access")
|
||||
prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path)
|
||||
if base_script:
|
||||
debug_observe("+", base_script, indent+1)
|
||||
else:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
prints(name, obj, obj.get_class(), obj.get_name())
|
||||
|
||||
|
||||
static func guard_instance(obj :Object) -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
|
||||
if Engine.has_meta(tag):
|
||||
return
|
||||
debug_observe("Gard on instance", obj)
|
||||
Engine.set_meta(tag, obj)
|
||||
|
||||
|
||||
static func unguard_instance(obj :Object, verbose := true) -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
|
||||
if verbose:
|
||||
debug_observe("unguard instance", obj)
|
||||
if Engine.has_meta(tag):
|
||||
Engine.remove_meta(tag)
|
||||
|
||||
|
||||
static func gc_guarded_instance(name :String, instance :Object) -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
unguard_instance(instance, false)
|
||||
if is_instance_valid(instance) and instance is RefCounted:
|
||||
# finally do this very hacky stuff
|
||||
# we need to manually unreferece to avoid leaked scripts
|
||||
# but still leaked GDScriptFunctionState exists
|
||||
#var script :GDScript = instance.get_script()
|
||||
#if script:
|
||||
# var base_script :GDScript = script.get_base_script()
|
||||
# if base_script:
|
||||
# base_script.unreference()
|
||||
debug_observe(name, instance)
|
||||
(instance as RefCounted).unreference()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
|
||||
|
||||
static func gc_on_guarded_instances() -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
for tag in Engine.get_meta_list():
|
||||
if tag.begins_with(TAG_OBSERVE_INSTANCE):
|
||||
var instance :Object = Engine.get_meta(tag)
|
||||
await gc_guarded_instance("Leaked instance detected:", instance)
|
||||
await GdUnitTools.free_instance(instance, false)
|
||||
|
||||
|
||||
# store the object into global store aswell to be verified by 'is_marked_auto_free'
|
||||
func _tag_object(obj :Variant) -> void:
|
||||
var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, [])
|
||||
tagged_object.append(obj)
|
||||
Engine.set_meta(TAG_AUTO_FREE, tagged_object)
|
||||
|
||||
|
||||
## Runs over all registered objects and releases them
|
||||
func gc() -> void:
|
||||
if _store.is_empty():
|
||||
return
|
||||
# give engine time to free objects to process objects marked by queue_free()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
if _is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size())
|
||||
var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
|
||||
while not _store.is_empty():
|
||||
var value :Variant = _store.pop_front()
|
||||
tagged_objects.erase(value)
|
||||
await GdUnitTools.free_instance(value, _is_stdout_verbose)
|
||||
assert(_store.is_empty(), "The memory observer has still entries in the store!")
|
||||
|
||||
|
||||
## Checks whether the specified object is registered for automatic release
|
||||
static func is_marked_auto_free(obj: Variant) -> bool:
|
||||
var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
|
||||
return tagged_objects.has(obj)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ibpnqu61f7yw
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Collects all reports seperated as warnings, failures and errors
|
||||
class_name GdUnitTestReportCollector
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var _reports :Array[GdUnitReport] = []
|
||||
|
||||
|
||||
static func __filter_is_error(report :GdUnitReport) -> bool:
|
||||
return report.is_error()
|
||||
|
||||
|
||||
static func __filter_is_failure(report :GdUnitReport) -> bool:
|
||||
return report.is_failure()
|
||||
|
||||
|
||||
static func __filter_is_warning(report :GdUnitReport) -> bool:
|
||||
return report.is_warning()
|
||||
|
||||
|
||||
static func __filter_is_skipped(report :GdUnitReport) -> bool:
|
||||
return report.is_skipped()
|
||||
|
||||
|
||||
static func count_failures(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_failure).size()
|
||||
|
||||
|
||||
static func count_errors(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_error).size()
|
||||
|
||||
|
||||
static func count_warnings(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_warning).size()
|
||||
|
||||
|
||||
static func count_skipped(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_skipped).size()
|
||||
|
||||
|
||||
func has_failures() -> bool:
|
||||
return _reports.any(__filter_is_failure)
|
||||
|
||||
|
||||
func has_errors() -> bool:
|
||||
return _reports.any(__filter_is_error)
|
||||
|
||||
|
||||
func has_warnings() -> bool:
|
||||
return _reports.any(__filter_is_warning)
|
||||
|
||||
|
||||
func has_skipped() -> bool:
|
||||
return _reports.any(__filter_is_skipped)
|
||||
|
||||
|
||||
func reports() -> Array[GdUnitReport]:
|
||||
return _reports
|
||||
|
||||
|
||||
func push_back(report :GdUnitReport) -> void:
|
||||
_reports.push_back(report)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cl13ejhh26vv7
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
## The executor to run a test-suite
|
||||
class_name GdUnitTestSuiteExecutor
|
||||
|
||||
|
||||
# preload all asserts here
|
||||
@warning_ignore("unused_private_class_variable")
|
||||
var _assertions := GdUnitAssertions.new()
|
||||
var _executeStage := GdUnitTestSuiteExecutionStage.new()
|
||||
var _debug_mode : bool
|
||||
|
||||
func _init(debug_mode :bool = false) -> void:
|
||||
_executeStage.set_debug_mode(debug_mode)
|
||||
_debug_mode = debug_mode
|
||||
|
||||
|
||||
func execute(test_suite :GdUnitTestSuite) -> void:
|
||||
var orphan_detection_enabled := GdUnitSettings.is_verbose_orphans()
|
||||
if not orphan_detection_enabled:
|
||||
prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.")
|
||||
|
||||
(Engine.get_main_loop() as SceneTree).root.call_deferred("add_child", test_suite)
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite))
|
||||
|
||||
|
||||
func run_and_wait(tests: Array[GdUnitTestCase]) -> void:
|
||||
if !_debug_mode:
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitInit.new())
|
||||
# first we group all tests by resource path
|
||||
var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String:
|
||||
return test.suite_resource_path
|
||||
)
|
||||
var scanner := GdUnitTestSuiteScanner.new()
|
||||
for suite_path: String in grouped_by_suites.keys():
|
||||
@warning_ignore("unsafe_call_argument")
|
||||
var suite_tests: Array[GdUnitTestCase] = Array(grouped_by_suites[suite_path], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(suite_path)
|
||||
if script.get_class() == "GDScript":
|
||||
var test_suite := scanner.load_suite(script as GDScript, suite_tests)
|
||||
await execute(test_suite)
|
||||
else:
|
||||
await GdUnit4CSharpApiLoader.execute(suite_tests)
|
||||
if !_debug_mode:
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitStop.new())
|
||||
|
||||
|
||||
func fail_fast(enabled :bool) -> void:
|
||||
_executeStage.fail_fast(enabled)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://hl8otc6pepsh
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
## The test case shutdown hook implementation.[br]
|
||||
## It executes the 'test_after()' block from the test-suite.
|
||||
class_name GdUnitTestCaseAfterStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
var _call_stage: bool
|
||||
|
||||
|
||||
func _init(call_stage := true) -> void:
|
||||
_call_stage = call_stage
|
||||
|
||||
|
||||
func _execute(context: GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
if _call_stage:
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.after_test()
|
||||
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER)
|
||||
await context.error_monitor_stop()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddknkun7aw51d
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
## The test case startup hook implementation.[br]
|
||||
## It executes the 'test_before()' block from the test-suite.
|
||||
class_name GdUnitTestCaseBeforeStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
var _call_stage :bool
|
||||
|
||||
|
||||
func _init(call_stage := true) -> void:
|
||||
_call_stage = call_stage
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
if _call_stage:
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.before_test()
|
||||
context.error_monitor_start()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8gq3sb8q6xih
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
## The test case execution stage.[br]
|
||||
class_name GdUnitTestCaseExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
var _stage_single_test: IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new()
|
||||
var _stage_fuzzer_test: IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new()
|
||||
|
||||
|
||||
## Executes the test case 'test_<name>()'.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> test_before() [br]
|
||||
## -> test_case() [br]
|
||||
## -> test_after() [br]
|
||||
@warning_ignore("redundant_await")
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_case := context.test_case
|
||||
|
||||
context.error_monitor_start()
|
||||
|
||||
if test_case.is_fuzzed():
|
||||
await _stage_fuzzer_test.execute(context)
|
||||
else:
|
||||
await _stage_single_test.execute(context)
|
||||
|
||||
await context.gc()
|
||||
await context.error_monitor_stop()
|
||||
|
||||
# finally free the test instance
|
||||
if is_instance_valid(context.test_case):
|
||||
context.test_case.dispose()
|
||||
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_single_test.set_debug_mode(debug_mode)
|
||||
_stage_fuzzer_test.set_debug_mode(debug_mode)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://brfrhige0dbmm
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
## The test suite shutdown hook implementation.[br]
|
||||
## It executes the 'after()' block from the test-suite.
|
||||
class_name GdUnitTestSuiteAfterStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.after()
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER)
|
||||
|
||||
var reports := context.collect_reports(false)
|
||||
var statistics := context.calculate_statistics(reports)
|
||||
fire_event(GdUnitEvent.new()\
|
||||
.suite_after(context.get_test_suite_path(),\
|
||||
test_suite.get_name(),
|
||||
statistics,
|
||||
reports))
|
||||
GdUnitFileAccess.clear_tmp()
|
||||
# Guard that checks if all doubled (spy/mock) objects are released
|
||||
await GdUnitClassDoubler.check_leaked_instances()
|
||||
# we hide the scene/main window after runner is finished
|
||||
if not Engine.is_embedded_in_editor():
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://vs73mmj8rsbs
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
## The test suite startup hook implementation.[br]
|
||||
## It executes the 'before()' block from the test-suite.
|
||||
class_name GdUnitTestSuiteBeforeStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
fire_event(GdUnitEvent.new()\
|
||||
.suite_before(context.get_test_suite_path(), test_suite.get_name(), test_suite.get_child_count()))
|
||||
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.before()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ce78xguk84kwb
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
## The test suite main execution stage.[br]
|
||||
class_name GdUnitTestSuiteExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new()
|
||||
var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new()
|
||||
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new()
|
||||
var _fail_fast := false
|
||||
|
||||
|
||||
## Executes all tests of an test suite.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> before() [br]
|
||||
## -> run all test cases [br]
|
||||
## -> after() [br]
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
if context.test_suite.__is_skipped:
|
||||
await fire_test_suite_skipped(context)
|
||||
else:
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter)
|
||||
await _stage_before.execute(context)
|
||||
for test_case_index in context.test_suite.get_child_count():
|
||||
# iterate only over test cases
|
||||
var test_case := context.test_suite.get_child(test_case_index) as _TestCase
|
||||
if not is_instance_valid(test_case):
|
||||
continue
|
||||
context.test_suite.set_active_test_case(test_case.test_name())
|
||||
await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case))
|
||||
# stop on first error or if fail fast is enabled
|
||||
if _fail_fast and not context.is_success():
|
||||
break
|
||||
if test_case.is_interupted():
|
||||
# it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out
|
||||
# we delete the current test suite where is execute the current test case to kill the function state
|
||||
# and replace it by a clone without function state
|
||||
context.test_suite = await clone_test_suite(context.test_suite)
|
||||
await _stage_after.execute(context)
|
||||
GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter)
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
context.test_suite.free()
|
||||
context.dispose()
|
||||
|
||||
|
||||
# clones a test suite and moves the test cases to new instance
|
||||
func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite:
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
dispose_timers(test_suite)
|
||||
await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter)
|
||||
var parent := test_suite.get_parent()
|
||||
var _test_suite := GdUnitTestSuite.new()
|
||||
parent.remove_child(test_suite)
|
||||
copy_properties(test_suite, _test_suite)
|
||||
for child in test_suite.get_children():
|
||||
test_suite.remove_child(child)
|
||||
_test_suite.add_child(child)
|
||||
parent.add_child(_test_suite)
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter)
|
||||
# finally free current test suite instance
|
||||
test_suite.free()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
return _test_suite
|
||||
|
||||
|
||||
func dispose_timers(test_suite :GdUnitTestSuite) -> void:
|
||||
GdUnitTools.release_timers()
|
||||
for child in test_suite.get_children():
|
||||
if child is Timer:
|
||||
(child as Timer).stop()
|
||||
test_suite.remove_child(child)
|
||||
child.free()
|
||||
|
||||
|
||||
func copy_properties(source :Object, target :Object) -> void:
|
||||
if not source is _TestCase and not source is GdUnitTestSuite:
|
||||
return
|
||||
for property in source.get_property_list():
|
||||
var property_name :String = property["name"]
|
||||
if property_name == "__awaiter":
|
||||
continue
|
||||
target.set(property_name, source.get(property_name))
|
||||
|
||||
|
||||
func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
var skip_count := test_suite.get_child_count()
|
||||
fire_event(GdUnitEvent.new()\
|
||||
.suite_before(context.get_test_suite_path(), test_suite.get_name(), skip_count))
|
||||
|
||||
|
||||
for test_case_index in context.test_suite.get_child_count():
|
||||
# iterate only over test cases
|
||||
var test_case := context.test_suite.get_child(test_case_index) as _TestCase
|
||||
if not is_instance_valid(test_case):
|
||||
continue
|
||||
var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case)
|
||||
fire_event(GdUnitEvent.new().test_before(test_case.id()))
|
||||
# use skip count 0 because we counted it over the complete test suite
|
||||
fire_test_skipped(test_case_context, 0)
|
||||
|
||||
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED_COUNT: skip_count,
|
||||
GdUnitEvent.SKIPPED: true
|
||||
}
|
||||
var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count))
|
||||
fire_event(GdUnitEvent.new().suite_after(context.get_test_suite_path(), test_suite.get_name(), statistics, [report]))
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
|
||||
|
||||
func fire_test_skipped(context: GdUnitExecutionContext, skip_count := 1) -> void:
|
||||
var test_case := context.test_case
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED: true,
|
||||
GdUnitEvent.SKIPPED_COUNT: skip_count,
|
||||
}
|
||||
var report := GdUnitReport.new() \
|
||||
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped("Skipped from the entire test suite"))
|
||||
fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report]))
|
||||
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_before.set_debug_mode(debug_mode)
|
||||
_stage_after.set_debug_mode(debug_mode)
|
||||
_stage_test.set_debug_mode(debug_mode)
|
||||
|
||||
|
||||
func fail_fast(enabled :bool) -> void:
|
||||
_fail_fast = enabled
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bfbyfr8ocwivm
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
## The interface of execution stage.[br]
|
||||
## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br]
|
||||
## Execution stage are always called synchronously.
|
||||
class_name IGdUnitExecutionStage
|
||||
extends RefCounted
|
||||
|
||||
var _debug_mode := false
|
||||
|
||||
|
||||
## Executes synchronized the implemented stage in its own execution context.[br]
|
||||
## example:[br]
|
||||
## [codeblock]
|
||||
## # waits for 100ms
|
||||
## await MyExecutionStage.new().execute(<GdUnitExecutionContext>)
|
||||
## [/codeblock][br]
|
||||
func execute(context :GdUnitExecutionContext) -> void:
|
||||
GdUnitThreadManager.get_current_context().set_execution_context(context)
|
||||
@warning_ignore("redundant_await")
|
||||
await _execute(context)
|
||||
|
||||
|
||||
## Sends the event to registered listeners
|
||||
func fire_event(event :GdUnitEvent) -> void:
|
||||
if _debug_mode:
|
||||
GdUnitSignals.instance().gdunit_event_debug.emit(event)
|
||||
else:
|
||||
GdUnitSignals.instance().gdunit_event.emit(event)
|
||||
|
||||
|
||||
## Internal testing stuff.[br]
|
||||
## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug`
|
||||
func set_debug_mode(debug_mode :bool) -> void:
|
||||
_debug_mode = debug_mode
|
||||
|
||||
|
||||
## The execution phase to be carried out.
|
||||
func _execute(_context :GdUnitExecutionContext) -> void:
|
||||
@warning_ignore("assert_always_false")
|
||||
assert(false, "The execution stage is not implemented")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://blqjb8vicbune
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
## The test case execution stage.[br]
|
||||
class_name GdUnitTestCaseFuzzedExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false)
|
||||
var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false)
|
||||
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new()
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
fire_event(GdUnitEvent.new().test_before(context.test_case.id()))
|
||||
|
||||
while context.retry_execution():
|
||||
var test_context := GdUnitExecutionContext.of(context)
|
||||
await _stage_before.execute(test_context)
|
||||
if not context.test_case.is_skipped():
|
||||
await _stage_test.execute(GdUnitExecutionContext.of(test_context))
|
||||
await _stage_after.execute(test_context)
|
||||
if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted():
|
||||
break
|
||||
|
||||
context.gc()
|
||||
if context.is_skipped():
|
||||
fire_test_skipped(context)
|
||||
else:
|
||||
var reports: = context.collect_reports(true)
|
||||
var statistics := context.calculate_statistics(reports)
|
||||
fire_event(GdUnitEvent.new().test_after(context.test_case.id(), statistics, reports))
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_before.set_debug_mode(debug_mode)
|
||||
_stage_after.set_debug_mode(debug_mode)
|
||||
_stage_test.set_debug_mode(debug_mode)
|
||||
|
||||
|
||||
func fire_test_skipped(context: GdUnitExecutionContext) -> void:
|
||||
var test_case := context.test_case
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED: true,
|
||||
GdUnitEvent.SKIPPED_COUNT: 1,
|
||||
}
|
||||
var report := GdUnitReport.new() \
|
||||
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info()))
|
||||
fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report]))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bur2on601qwvw
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
## The fuzzed test case execution stage.[br]
|
||||
class_name GdUnitTestCaseFuzzedTestStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
var _expression_runner := GdUnitExpressionRunner.new()
|
||||
|
||||
|
||||
## Executes a test case with given fuzzers 'test_<name>(<fuzzer>)' iterative.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> test_case() [br]
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
var test_case := context.test_case
|
||||
var fuzzers := create_fuzzers(test_suite, test_case)
|
||||
|
||||
# guard on fuzzers
|
||||
for fuzzer in fuzzers:
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitMemoryObserver.guard_instance(fuzzer)
|
||||
|
||||
for iteration in test_case.iterations():
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.before_test()
|
||||
await test_case.execute(fuzzers, iteration)
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.after_test()
|
||||
if test_case.is_interupted():
|
||||
break
|
||||
# interrupt at first failure
|
||||
var reports := context.reports()
|
||||
if not reports.is_empty():
|
||||
var report :GdUnitReport = reports.pop_front()
|
||||
reports.append(GdUnitReport.new() \
|
||||
.create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message())))
|
||||
break
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE)
|
||||
|
||||
# unguard on fuzzers
|
||||
if not test_case.is_interupted():
|
||||
for fuzzer in fuzzers:
|
||||
GdUnitMemoryObserver.unguard_instance(fuzzer)
|
||||
|
||||
|
||||
func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]:
|
||||
if not test_case.is_fuzzed():
|
||||
return Array()
|
||||
test_case.generate_seed()
|
||||
var fuzzers :Array[Fuzzer] = []
|
||||
for fuzzer_arg in test_case.fuzzer_arguments():
|
||||
@warning_ignore("unsafe_cast")
|
||||
var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script() as GDScript, fuzzer_arg.plain_value() as String)
|
||||
fuzzer._iteration_index = 0
|
||||
fuzzer._iteration_limit = test_case.iterations()
|
||||
fuzzers.append(fuzzer)
|
||||
return fuzzers
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dky6221ssl6re
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
## The test case execution stage.[br]
|
||||
class_name GdUnitTestCaseSingleExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new()
|
||||
var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new()
|
||||
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new()
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
fire_event(GdUnitEvent.new().test_before(context.test_case.id()))
|
||||
while context.retry_execution():
|
||||
var test_context := GdUnitExecutionContext.of(context)
|
||||
await _stage_before.execute(test_context)
|
||||
if not test_context.is_skipped():
|
||||
await _stage_test.execute(GdUnitExecutionContext.of(test_context))
|
||||
await _stage_after.execute(test_context)
|
||||
if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted():
|
||||
break
|
||||
|
||||
context.gc()
|
||||
if context.is_skipped():
|
||||
fire_test_skipped(context)
|
||||
else:
|
||||
var reports: = context.collect_reports(true)
|
||||
var statistics := context.calculate_statistics(reports)
|
||||
fire_event(GdUnitEvent.new().test_after(context.test_case.id(), statistics, reports))
|
||||
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_before.set_debug_mode(debug_mode)
|
||||
_stage_after.set_debug_mode(debug_mode)
|
||||
_stage_test.set_debug_mode(debug_mode)
|
||||
|
||||
|
||||
func fire_test_skipped(context: GdUnitExecutionContext) -> void:
|
||||
var test_case := context.test_case
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED: true,
|
||||
GdUnitEvent.SKIPPED_COUNT: 1,
|
||||
}
|
||||
var report := GdUnitReport.new() \
|
||||
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info()))
|
||||
fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report]))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckbcmvbm3bee8
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user