diff options
| author | radiohotline <radoihotline@disroot.rg> | 2026-08-25 16:33:56 +0300 |
|---|---|---|
| committer | radiohotline <radoihotline@disroot.rg> | 2026-08-25 16:33:56 +0300 |
| commit | a89e744ed0e8c759caf041a580112365bbfe4303 (patch) | |
| tree | dd61e320904176995727e86a857d00c90d4933ba /addons/gdUnit4/src/monitor | |
| parent | 6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff) | |
Diffstat (limited to 'addons/gdUnit4/src/monitor')
| -rw-r--r-- | addons/gdUnit4/src/monitor/ErrorLogEntry.gd | 41 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid | 1 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GdUnitMonitor.gd | 24 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid | 1 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd | 70 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid | 1 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd | 221 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid | 1 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd | 122 | ||||
| -rw-r--r-- | addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid | 1 |
10 files changed, 483 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd new file mode 100644 index 0000000..ebbc93f --- /dev/null +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -0,0 +1,41 @@ +extends RefCounted +class_name ErrorLogEntry + + +enum TYPE { + SCRIPT_ERROR, + PUSH_ERROR, + PUSH_WARNING +} + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _type: TYPE +var _line: int +var _message: String +var _details: String + + +func _init(type: TYPE, line: int, message: String, details: String) -> void: + _type = type + _line = line + _message = message + _details = details + + +func _to_string() -> String: + return _message + + +static func of_push_warning(line: int, message: String, stack_trace: PackedStringArray) -> ErrorLogEntry: + return ErrorLogEntry.new(TYPE.PUSH_WARNING, line, message, "\n".join(stack_trace)) + + +static func of_push_error(line: int, message: String, stack_trace: PackedStringArray) -> ErrorLogEntry: + return ErrorLogEntry.new(TYPE.PUSH_ERROR, line, message, "\n".join(stack_trace)) + + +static func of_script_error(line: int, message: String, stack_trace: PackedStringArray) -> ErrorLogEntry: + return ErrorLogEntry.new(TYPE.SCRIPT_ERROR, line, message, "\n".join(stack_trace)) diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid new file mode 100644 index 0000000..4025709 --- /dev/null +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid @@ -0,0 +1 @@ +uid://ca534u63pypt7 diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd new file mode 100644 index 0000000..b6429ca --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd @@ -0,0 +1,24 @@ +# GdUnit Monitoring Base Class +class_name GdUnitMonitor +extends RefCounted + +var _id :String + +# constructs new Monitor with given id +func _init(p_id :String) -> void: + _id = p_id + + +# Returns the id of the monitor to uniqe identify +func id() -> String: + return _id + + +# starts monitoring +func start() -> void: + pass + + +# stops monitoring +func stop() -> void: + pass diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid new file mode 100644 index 0000000..b4875fd --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid @@ -0,0 +1 @@ +uid://bd5amxrxcckjv diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd new file mode 100644 index 0000000..c195760 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd @@ -0,0 +1,70 @@ +class_name GdUnitOrphanNodeInfo +extends RefCounted + +enum GdUnitOrphanType { + member, + variable, + unknown +} + + +var _id: int +var _orphan_type: GdUnitOrphanType +var _type: String +var _name: String +var _script_ref: String +var _func_ref: String +var _next: GdUnitOrphanNodeInfo + +const text_color := Color.ANTIQUE_WHITE +const function_color := Color.SKY_BLUE +const member_variable_color := Color.SALMON +const engine_type_color := Color.LIGHT_GREEN +const script_path_color := Color.CORNFLOWER_BLUE + + +func _init(orphan_type: GdUnitOrphanType, id: int, type: String, name: String, script_ref: String, func_ref: String = "") -> void: + _orphan_type = orphan_type + _id = id + _type = type + _name = name + _script_ref = script_ref + _func_ref = func_ref + + +func as_trace(info: GdUnitOrphanNodeInfo, show_orphan_id := true) -> String: + var trace := "" + if show_orphan_id: + trace += "• <%s> Id:%s\n" % [ + _colored(info._type, engine_type_color), + _colored(info._id, engine_type_color)] + match info._orphan_type: + GdUnitOrphanType.member: + return trace + " at %s script: %s" % [ + _colored(info._name, member_variable_color), + _colored(info._script_ref, script_path_color) + ] + sub_info(info._next) + GdUnitOrphanType.variable: + return trace + " at %s script: %s.%s()" % [ + _colored(info._name, member_variable_color), + _colored(info._script_ref, script_path_color), + _colored(info._func_ref, function_color), + ] + GdUnitOrphanType.unknown: + return trace + " %s" % [ + _colored(info._name, member_variable_color) + ] + + _: + return trace + " No details available" + + +func sub_info(next: GdUnitOrphanNodeInfo) -> String: + if next == null: + return "" + + return "\n" + as_trace(next, false) + + +static func _colored(value: Variant, color: Color) -> String: + return "[color=%s]%s[/color]" % [color.to_html(), value] diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid new file mode 100644 index 0000000..a74b4e2 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodeInfo.gd.uid @@ -0,0 +1 @@ +uid://lc78iyt2b13e diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd new file mode 100644 index 0000000..05f11cd --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd @@ -0,0 +1,221 @@ +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() diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid new file mode 100644 index 0000000..42c79c7 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid @@ -0,0 +1 @@ +uid://ckqlijgdcnfno diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd new file mode 100644 index 0000000..b1b28d7 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd @@ -0,0 +1,122 @@ +class_name GodotGdErrorMonitor +extends GdUnitMonitor + + +var _logger: GdUnitLogger + + +class GdUnitLogger extends Logger: + var _entries: Array[ErrorLogEntry] = [] + var _line_number: int + var _is_report_push_errors: bool + var _is_report_script_errors: bool + + + func _init(is_report_push_errors: bool, is_report_script_errors: bool) -> void: + _is_report_push_errors = is_report_push_errors + _is_report_script_errors = is_report_script_errors + OS.add_logger(self) + + + func entries() -> Array[ErrorLogEntry]: + return _entries + + func erase_log_entry(log_entry: ErrorLogEntry) -> void: + for entry in _entries: + if entry._type == log_entry._type and entry._message == log_entry._message: + _entries.erase(entry) + return + + + func _log_error( + _function: String, + _file: String, + _line: int, + message: String, + _rationale: String, + _editor_notify: bool, + error_type: int, + script_backtraces: Array[ScriptBacktrace] + ) -> void: + match error_type: + ErrorType.ERROR_TYPE_WARNING: + if _is_report_push_errors: + var stack_trace := _build_stack_trace(script_backtraces) + _entries.append(ErrorLogEntry.of_push_warning(_line_number, message, stack_trace)) + + ErrorType.ERROR_TYPE_ERROR: + if _is_report_push_errors: + var stack_trace := _build_stack_trace(script_backtraces) + _entries.append(ErrorLogEntry.of_push_error(_line_number, message, stack_trace)) + + ErrorType.ERROR_TYPE_SCRIPT: + if _is_report_script_errors: + var stack_trace := _build_stack_trace(script_backtraces) + _entries.append(ErrorLogEntry.of_script_error(_line_number, message, stack_trace)) + + ErrorType.ERROR_TYPE_SHADER: + pass + _: + prints("Unknwon log type", message) + + func _log_message(_message: String, _error: bool) -> void: + pass + + func _build_stack_trace(script_backtraces: Array[ScriptBacktrace]) -> PackedStringArray: + for sb in script_backtraces: + for frame in sb.get_frame_count(): + # Find start of test stack + if sb.get_frame_file(frame) == "res://addons/gdUnit4/src/core/_TestCase.gd": + var stack_trace := PackedStringArray() + for test_case_frame in range(0, frame): + _line_number = sb.get_frame_line(test_case_frame) + stack_trace.append(" at %s:%s" % [sb.get_frame_file(test_case_frame), sb.get_frame_line(test_case_frame)]) + return stack_trace + # if no stack trace collected, we in an await function call + var sb := script_backtraces[0] + return [" at %s:%s" % [sb.get_frame_file(0), sb.get_frame_line(0)]] + + +func _init() -> void: + super("GdUnitLoggerMonitor") + _logger = GdUnitLogger.new(GdUnitSettings.is_report_push_errors(), GdUnitSettings.is_report_script_errors()) + + +func start() -> void: + clear_logs() + + +func stop() -> void: + pass + + +func log_entries() -> Array[ErrorLogEntry]: + return _logger.entries() + + +func erase_log_entry(log_entry: ErrorLogEntry) -> void: + _logger.erase_log_entry(log_entry) + + +func to_reports() -> Array[GdUnitReport]: + var reports_: Array[GdUnitReport] = [] + + reports_.assign(log_entries().map(_to_report)) + + return reports_ + + +static func _to_report(errorLog: ErrorLogEntry) -> GdUnitReport: + var failure := """ + %s + %s %s + %s""".dedent().trim_prefix("\n") % [ + GdAssertMessages._error("Godot Runtime Error !"), + GdAssertMessages._error("Error:"), + GdAssertMessages._colored_value(errorLog._message), + GdAssertMessages._colored(errorLog._details, GdAssertMessages.VALUE_COLOR)] + return GdUnitReport.new().create(GdUnitReport.ABORT, errorLog._line, failure) + + +func clear_logs() -> void: + log_entries().clear() diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid new file mode 100644 index 0000000..1b1e96b --- /dev/null +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid @@ -0,0 +1 @@ +uid://bhessnp2uk5no |
