neon-sprawl/client/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd

222 lines
7.3 KiB
GDScript

class_name GdUnitOrphanNodesMonitor
extends GdUnitMonitor
const excluded_frame_files: PackedStringArray = [
"GdUnitOrphanNodesMonitor",
"GdUnitExecutionContext",
"_TestCase",
"IGdUnitExecutionStage",
"GdUnitTestCaseSingleTestStage",
"GdUnitTestCaseSingleExecutionStage",
"GdUnitTestCaseExecutionStage",
"GdUnitTestSuiteExecutionStage",
"GdUnitTestSuiteExecutor"
]
var _child_monitors: Array[GdUnitOrphanNodesMonitor] = []
var _orphan_detection_enabled :bool
var _initial_orphans: Array[int] = []
var _orphan_ids_at_start: Array[int] = []
var _orphan_ids_at_stop: Array[int] = []
var _collected_orphan_infos: Array[GdUnitOrphanNodeInfo] = []
func _init(name: String) -> void:
super("OrphanNodesMonitor:" + name)
_orphan_detection_enabled = GdUnitSettings.is_verbose_orphans()
_initial_orphans = _get_orphan_node_ids()
func add_child_monitor(monitor: GdUnitOrphanNodesMonitor) -> void:
if not _orphan_detection_enabled:
return
_child_monitors.append(monitor)
func start() -> void:
if not _orphan_detection_enabled:
return
_collected_orphan_infos.clear()
# Collect current orphan id's to be filtered out at `stop`
_orphan_ids_at_start = _get_orphan_node_ids()
func stop() -> void:
if not _orphan_detection_enabled:
return
# Collect only new detected orphan id's, we want only to collect orphans between start and stop time
_orphan_ids_at_stop = _get_orphan_node_ids().filter(func(element: int) -> bool:
# Excluding sub monitores orphans
if _collect_child_orphan_ids().has(element):
return false
# Excluding orphans at start
return not _orphan_ids_at_start.has(element) and not _initial_orphans.has(element)
)
func _collect_child_orphan_ids() -> Array[int]:
var collected_ids: Array[int] = []
for child_monitor in _child_monitors:
collected_ids.append_array(child_monitor._orphan_ids_at_stop)
collected_ids.append_array(child_monitor._collect_child_orphan_ids())
return collected_ids
func detected_orphans() -> Array[GdUnitOrphanNodeInfo]:
if not _orphan_detection_enabled:
return []
return _collected_orphan_infos.filter(func(info: GdUnitOrphanNodeInfo) -> bool:
return info._id in _orphan_ids_at_stop
)
func orphans_count() -> int:
if not _orphan_detection_enabled:
return 0
return _orphan_ids_at_stop.size()
func collect() -> void:
if not _orphan_detection_enabled:
return
for orphan_id in _get_orphan_node_ids():
var orphan_to_find := instance_from_id(orphan_id)
_collect_orphan_info(orphan_to_find)
func _collect_orphan_info(orphan_to_find: Object) -> void:
if orphan_to_find == null:
return
var orphan_node := _find_orphan_on_backtraces(orphan_to_find)
if orphan_node:
_collected_orphan_infos.append(orphan_node)
return
if Engine.has_meta("GdUnitSceneRunner"):
var current_scene_runner:GdUnitSceneRunner = Engine.get_meta("GdUnitSceneRunner")
if is_instance_valid(current_scene_runner):
orphan_node = _find_orphan_at_node(orphan_to_find, current_scene_runner.scene())
if orphan_node:
_collected_orphan_infos.append(orphan_node)
return
# not able to find the orphan node via backtrace loaded nodeds
var message := "No details found. Verify called functions manually."
if not EngineDebugger.is_active():
message = "No details available. [color=yellow]Run tests in debug mode to collect details.[/color]"
_collected_orphan_infos.append(GdUnitOrphanNodeInfo.new(
GdUnitOrphanNodeInfo.GdUnitOrphanType.unknown,
orphan_to_find.get_instance_id(),
orphan_to_find.get_class(),
message,
""))
func _find_orphan_at_node(orphan_to_find: Object, node: Node) -> GdUnitOrphanNodeInfo:
var script: Script = node.get_script()
if script is not GDScript:
return null
# First search over all properties
for property in script.get_script_property_list():
var property_name: String = property["name"]
var property_type: int = property["type"]
# Is untyped or type object
if property_type in [TYPE_NIL, TYPE_OBJECT]:
var property_instance: Variant = node.get(property_name)
@warning_ignore("unsafe_cast")
var property_as_node := property_instance as Node if property_instance != null else null
if property_as_node == null:
continue
if property_as_node == orphan_to_find:
return GdUnitOrphanNodeInfo.new(
GdUnitOrphanNodeInfo.GdUnitOrphanType.member,
orphan_to_find.get_instance_id(),
orphan_to_find.get_class(),
property_name,
script.resource_path)
# Search on node childs
var orphan_node_info := _find_orphan_at_node(orphan_to_find, property_as_node)
if orphan_node_info:
orphan_node_info._next = GdUnitOrphanNodeInfo.new(
GdUnitOrphanNodeInfo.GdUnitOrphanType.member,
orphan_to_find.get_instance_id(),
orphan_to_find.get_class(),
property_name,
script.resource_path)
return orphan_node_info
# Second over all children
for child_node in node.get_children():
var orphan_node_info := _find_orphan_at_node(orphan_to_find, child_node)
if orphan_node_info:
return orphan_node_info
return null
func _is_frame_file_excluded(frame_file: String) -> bool:
for file in excluded_frame_files:
if frame_file.contains(file):
return true
return false
func _find_orphan_on_backtraces(orphan_to_find: Object) -> GdUnitOrphanNodeInfo:
for script_backtrace in Engine.capture_script_backtraces(true):
for frame in script_backtrace.get_frame_count():
var frame_file := script_backtrace.get_frame_file(frame)
if _is_frame_file_excluded(frame_file):
continue
# Scan function variables
for l_index in script_backtrace.get_local_variable_count(frame):
var variable_instance: Variant = script_backtrace.get_local_variable_value(frame, l_index)
var variable_name := script_backtrace.get_local_variable_name(frame, l_index)
if typeof(variable_instance) in [TYPE_NIL, TYPE_OBJECT]:
@warning_ignore("unsafe_cast")
var node := variable_instance as Node
if node == null:
continue
if variable_instance == orphan_to_find:
return GdUnitOrphanNodeInfo.new(
GdUnitOrphanNodeInfo.GdUnitOrphanType.variable,
orphan_to_find.get_instance_id(),
orphan_to_find.get_class(),
variable_name,
script_backtrace.get_frame_file(frame),
script_backtrace.get_frame_function(frame))
else:
var orphan_node_info := _find_orphan_at_node(orphan_to_find, node)
if orphan_node_info:
return orphan_node_info
# Scan class members
for m_index in script_backtrace.get_member_variable_count(frame):
var member_instance: Variant = script_backtrace.get_member_variable_value(frame, m_index)
var member_name := script_backtrace.get_member_variable_name(frame, m_index)
if typeof(member_instance) in [TYPE_NIL, TYPE_OBJECT]:
@warning_ignore("unsafe_cast")
var node := member_instance as Node
if node == null:
continue
if member_instance == orphan_to_find:
return GdUnitOrphanNodeInfo.new(
GdUnitOrphanNodeInfo.GdUnitOrphanType.member,
orphan_to_find.get_instance_id(),
orphan_to_find.get_class(),
member_name,
script_backtrace.get_frame_file(frame))
else:
var orphan_node_info := _find_orphan_at_node(orphan_to_find, node)
if orphan_node_info:
return orphan_node_info
return null
static func _get_orphan_node_ids() -> Array[int]:
@warning_ignore("unsafe_property_access", "unsafe_method_access")
return Engine.get_main_loop().root.get_orphan_node_ids()