summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
diff options
context:
space:
mode:
authorradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
committerradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
commita89e744ed0e8c759caf041a580112365bbfe4303 (patch)
treedd61e320904176995727e86a857d00c90d4933ba /addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
parent6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff)
escoria updateHEADmain
Diffstat (limited to 'addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd')
-rw-r--r--addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd1210
1 files changed, 1210 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
new file mode 100644
index 0000000..f55dbee
--- /dev/null
+++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd
@@ -0,0 +1,1210 @@
+@tool
+extends VSplitContainer
+
+## Will be emitted when the test index counter is changed
+signal test_counters_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE)
+signal tree_item_selected(item: TreeItem)
+
+
+@onready var _tree: Tree = $Panel/Tree
+@onready var _report_list: Node = $report/ScrollContainer/list
+@onready var _report_template: RichTextLabel = $report/report_template
+@onready var _context_menu: GdUnitInspectorContextMenu = $contextMenu
+@onready var _discover_hint: Control = %discover_hint
+@onready var _spinner: Button = %spinner
+
+# loading tree icons
+@onready var ICON_SPINNER := GdUnitUiTools.get_spinner()
+@onready var ICON_FOLDER := GdUnitUiTools.get_icon("Folder")
+# gdscript icons
+@onready var ICON_GDSCRIPT_TEST_DEFAULT := GdUnitUiTools.get_icon("GDScript", Color.LIGHT_GRAY)
+@onready var ICON_GDSCRIPT_TEST_SUCCESS := GdUnitUiTools.get_GDScript_icon("StatusSuccess", Color.DARK_GREEN)
+@onready var ICON_GDSCRIPT_TEST_FLAKY := GdUnitUiTools.get_GDScript_icon("CheckBox", Color.GREEN_YELLOW)
+@onready var ICON_GDSCRIPT_TEST_FAILED := GdUnitUiTools.get_GDScript_icon("StatusError", Color.SKY_BLUE)
+@onready var ICON_GDSCRIPT_TEST_ERROR := GdUnitUiTools.get_GDScript_icon("StatusError", Color.DARK_RED)
+@onready var ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.DARK_GREEN)
+@onready var ICON_GDSCRIPT_TEST_FAILED_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.SKY_BLUE)
+@onready var ICON_GDSCRIPT_TEST_ERRORS_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.DARK_RED)
+# csharp script icons
+@onready var ICON_CSSCRIPT_TEST_DEFAULT := GdUnitUiTools.get_icon("CSharpScript", Color.LIGHT_GRAY)
+@onready var ICON_CSSCRIPT_TEST_SUCCESS := GdUnitUiTools.get_CSharpScript_icon("StatusSuccess", Color.DARK_GREEN)
+@onready var ICON_CSSCRIPT_TEST_FAILED := GdUnitUiTools.get_CSharpScript_icon("StatusError", Color.SKY_BLUE)
+@onready var ICON_CSSCRIPT_TEST_ERROR := GdUnitUiTools.get_CSharpScript_icon("StatusError", Color.DARK_RED)
+@onready var ICON_CSSCRIPT_TEST_SUCCESS_ORPHAN := GdUnitUiTools.get_CSharpScript_icon("Unlinked", Color.DARK_GREEN)
+@onready var ICON_CSSCRIPT_TEST_FAILED_ORPHAN := GdUnitUiTools.get_CSharpScript_icon("Unlinked", Color.SKY_BLUE)
+@onready var ICON_CSSCRIPT_TEST_ERRORS_ORPHAN := GdUnitUiTools.get_CSharpScript_icon("Unlinked", Color.DARK_RED)
+
+
+enum GdUnitType {
+ FOLDER,
+ TEST_SUITE,
+ TEST_CASE,
+ TEST_GROUP
+}
+
+const META_GDUNIT_PROGRESS_COUNT_MAX := "gdUnit_progress_count_max"
+const META_GDUNIT_PROGRESS_INDEX := "gdUnit_progress_index"
+const META_TEST_CASE := "gdunit_test_case"
+const META_GDUNIT_NAME := "gdUnit_name"
+const META_GDUNIT_STATE := "gdUnit_state"
+const META_GDUNIT_TYPE := "gdUnit_type"
+const META_GDUNIT_SUCCESS_TESTS := "gdUnit_suite_success_tests"
+const META_GDUNIT_REPORT := "gdUnit_report"
+const META_GDUNIT_ORPHAN := "gdUnit_orphan"
+const META_GDUNIT_EXECUTION_TIME := "gdUnit_execution_time"
+const META_GDUNIT_ORIGINAL_INDEX = "gdunit_original_index"
+const STATE = GdUnitInspectorTreeConstants.STATE
+
+
+var _tree_root: TreeItem
+var _current_selected_item: TreeItem = null
+var _current_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode()
+var _run_test_recovery := true
+
+
+## Used for debugging purposes only
+func print_tree_item_ids(parent: TreeItem) -> TreeItem:
+ for child in parent.get_children():
+ if child.has_meta(META_TEST_CASE):
+ var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE)
+ prints(test_case.guid, test_case.test_name)
+
+ if child.get_child_count() > 0:
+ print_tree_item_ids(child)
+
+ return null
+
+
+func _find_tree_item(parent: TreeItem, item_name: String) -> TreeItem:
+ for child in parent.get_children():
+ if child.get_meta(META_GDUNIT_NAME) == item_name:
+ return child
+ return null
+
+
+func _find_tree_item_by_id(parent: TreeItem, id: GdUnitGUID) -> TreeItem:
+ for child in parent.get_children():
+ if is_test_id(child, id):
+ return child
+ if child.get_child_count() > 0:
+ var item := _find_tree_item_by_id(child, id)
+ if item != null:
+ return item
+
+ return null
+
+
+func _find_tree_item_by_test_suite(parent: TreeItem, suite_path: String, suite_name: String) -> TreeItem:
+ for child in parent.get_children():
+ if child.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE:
+ var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE)
+ if test_case.suite_resource_path == suite_path and test_case.suite_name == suite_name:
+ return child
+ if child.get_child_count() > 0:
+ var item := _find_tree_item_by_test_suite(child, suite_path, suite_name)
+ if item != null:
+ return item
+ return null
+
+
+func _find_first_item_by_state(parent: TreeItem, item_state: STATE, reverse := false) -> TreeItem:
+ var itmes := parent.get_children()
+ if reverse:
+ itmes.reverse()
+ for item in itmes:
+ if is_test_case(item) and (is_item_state(item, item_state)):
+ return item
+ var failure_item := _find_first_item_by_state(item, item_state, reverse)
+ if failure_item != null:
+ return failure_item
+ return null
+
+
+func _find_last_item_by_state(parent: TreeItem, item_state: STATE) -> TreeItem:
+ return _find_first_item_by_state(parent, item_state, true)
+
+
+func _find_item_by_state(current: TreeItem, item_state: STATE, prev := false) -> TreeItem:
+ var next := current.get_prev_in_tree() if prev else current.get_next_in_tree()
+ if next == null or next == _tree_root:
+ return null
+ if is_test_case(next) and is_item_state(next, item_state):
+ return next
+ return _find_item_by_state(next, item_state, prev)
+
+
+func is_item_state(item: TreeItem, item_state: STATE) -> bool:
+ return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == item_state
+
+
+func is_state_running(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.RUNNING)
+
+
+func is_state_success(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.SUCCESS)
+
+
+func is_state_warning(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.WARNING)
+
+
+func is_state_failed(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.FAILED)
+
+
+func is_state_error(item: TreeItem) -> bool:
+ return is_item_state(item, STATE.ERROR) or is_item_state(item, STATE.ABORDED)
+
+
+func is_item_state_orphan(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_ORPHAN)
+
+
+func is_test_suite(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE
+
+
+func is_test_case(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_CASE
+
+
+func is_folder(item: TreeItem) -> bool:
+ return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER
+
+
+func is_test_id(item: TreeItem, id: GdUnitGUID) -> bool:
+ if not item.has_meta(META_TEST_CASE):
+ return false
+
+ var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ return test_case.guid.equals(id)
+
+
+func disable_test_recovery() -> void:
+ _run_test_recovery = false
+
+
+@warning_ignore("return_value_discarded")
+func _ready() -> void:
+ if Engine.is_editor_hint():
+ var base_control := EditorInterface.get_base_control()
+ base_control.set_meta("GdUnit4Inspector", self)
+
+ _spinner.icon = GdUnitUiTools.get_spinner()
+ init_tree()
+ GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
+ GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
+ GdUnitSignals.instance().gdunit_test_discover_added.connect(on_test_case_discover_added)
+ GdUnitSignals.instance().gdunit_test_discover_deleted.connect(on_test_case_discover_deleted)
+ GdUnitSignals.instance().gdunit_test_discover_modified.connect(on_test_case_discover_modified)
+ if _run_test_recovery:
+ GdUnitTestDiscoverer.restore_last_session()
+
+
+# we need current to manually redraw bacause of the animation bug
+# https://github.com/godotengine/godot/issues/69330
+func _process(_delta: float) -> void:
+ if is_visible_in_tree():
+ queue_redraw()
+
+
+func init_tree() -> void:
+ cleanup_tree()
+ _tree.deselect_all()
+ _tree.set_hide_root(true)
+ _tree.ensure_cursor_is_visible()
+ _tree.set_allow_reselect(true)
+ _tree.set_allow_rmb_select(true)
+ _tree.set_columns(2)
+ _tree.set_column_clip_content(0, true)
+ _tree.set_column_expand_ratio(0, 1)
+ _tree.set_column_custom_minimum_width(0, 240)
+ _tree.set_column_expand_ratio(1, 0)
+ _tree.set_column_custom_minimum_width(1, 100)
+ _tree_root = _tree.create_item()
+ _tree_root.set_text(0, "tree_root")
+ _tree_root.set_meta(META_GDUNIT_NAME, "tree_root")
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0)
+ _tree_root.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
+ _tree_root.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
+ # fix tree icon scaling
+ var scale_factor := EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0
+ _tree.set("theme_override_constants/icon_max_width", 16 * scale_factor)
+
+
+func cleanup_tree() -> void:
+ clear_reports()
+ if not _tree_root:
+ return
+ _free_recursive()
+ _tree.clear()
+ _current_selected_item = null
+
+
+func _free_recursive(items:=_tree_root.get_children()) -> void:
+ for item in items:
+ _free_recursive(item.get_children())
+ item.call_deferred("free")
+
+
+func sort_tree_items(parent: TreeItem) -> void:
+ _sort_tree_items(parent, GdUnitSettings.get_inspector_tree_sort_mode())
+ _tree.queue_redraw()
+
+
+static func _sort_tree_items(parent: TreeItem, sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void:
+ parent.visible = false
+ var items := parent.get_children()
+ # first remove all childs before sorting
+ for item in items:
+ parent.remove_child(item)
+
+ # do sort by selected sort mode
+ match sort_mode:
+ GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED:
+ items.sort_custom(sort_items_by_original_index)
+
+ GdUnitInspectorTreeConstants.SORT_MODE.NAME_ASCENDING:
+ items.sort_custom(sort_items_by_name.bind(true))
+
+ GdUnitInspectorTreeConstants.SORT_MODE.NAME_DESCENDING:
+ items.sort_custom(sort_items_by_name.bind(false))
+
+ GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME:
+ items.sort_custom(sort_items_by_execution_time)
+
+ # readding sorted childs
+ for item in items:
+ parent.add_child(item)
+ if item.get_child_count() > 0:
+ _sort_tree_items(item, sort_mode)
+ parent.visible = true
+
+
+static func sort_items_by_name(a: TreeItem, b: TreeItem, ascending: bool) -> bool:
+ var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE)
+ var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE)
+
+ # Sort folders to the top
+ if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER:
+ return true
+ if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER:
+ return false
+
+ # sort by name
+ var name_a: String = a.get_meta(META_GDUNIT_NAME)
+ var name_b: String = b.get_meta(META_GDUNIT_NAME)
+ var comparison := name_a.naturalnocasecmp_to(name_b)
+
+ return comparison < 0 if ascending else comparison > 0
+
+
+static func sort_items_by_execution_time(a: TreeItem, b: TreeItem) -> bool:
+ var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE)
+ var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE)
+
+ # Sort folders to the top
+ if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER:
+ return true
+ if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER:
+ return false
+
+ var execution_time_a :int = a.get_meta(META_GDUNIT_EXECUTION_TIME)
+ var execution_time_b :int = b.get_meta(META_GDUNIT_EXECUTION_TIME)
+ # if has same execution time sort by name
+ if execution_time_a == execution_time_b:
+ var name_a :String = a.get_meta(META_GDUNIT_NAME)
+ var name_b :String = b.get_meta(META_GDUNIT_NAME)
+ return name_a.naturalnocasecmp_to(name_b) > 0
+ return execution_time_a > execution_time_b
+
+
+static func sort_items_by_original_index(a: TreeItem, b: TreeItem) -> bool:
+ var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE)
+ var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE)
+
+ # Sort folders to the top
+ if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER:
+ return true
+ if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER:
+ return false
+
+ var index_a :int = a.get_meta(META_GDUNIT_ORIGINAL_INDEX)
+ var index_b :int = b.get_meta(META_GDUNIT_ORIGINAL_INDEX)
+
+ # Sorting by index
+ return index_a < index_b
+
+
+func restructure_tree(parent: TreeItem, tree_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void:
+ _current_tree_view_mode = tree_mode
+
+ match tree_mode:
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT:
+ restructure_tree_to_flat(parent)
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE:
+ restructure_tree_to_tree(parent)
+ recalculate_counters(_tree_root)
+ # finally apply actual sort mode
+ sort_tree_items(_tree_root)
+
+
+# Restructure into flat mode
+func restructure_tree_to_flat(parent: TreeItem) -> void:
+ var folders := flatmap_folders(parent)
+ # Store current folder paths and their test suites
+ for folder_path: String in folders:
+ var test_suites: Array[TreeItem] = folders[folder_path]
+ if test_suites.is_empty():
+ continue
+
+ # Create flat folder and move test suites into it
+ var folder := _tree.create_item(parent)
+ folder.set_meta(META_GDUNIT_NAME, folder_path)
+ update_item_total_counter(folder)
+ set_state_initial(folder, GdUnitType.FOLDER)
+
+ # Move test suites under the flat folder
+ for test_suite in test_suites:
+ var old_parent := test_suite.get_parent()
+ old_parent.remove_child(test_suite)
+ folder.add_child(test_suite)
+
+ # Cleanup old folder structure
+ cleanup_empty_folders(parent)
+
+
+# Restructure into hierarchical tree mode
+func restructure_tree_to_tree(parent: TreeItem) -> void:
+ var items_to_process := parent.get_children().duplicate()
+
+ for item: TreeItem in items_to_process:
+ if is_folder(item):
+ var folder_path: String = item.get_meta(META_GDUNIT_NAME)
+ var parts := folder_path.split("/")
+
+ if parts.size() > 1:
+ var current_parent := parent
+ # Build folder hierarchy
+ for part in parts:
+ var next := _find_tree_item(current_parent, part)
+ if not next:
+ next = _tree.create_item(current_parent)
+ next.set_meta(META_GDUNIT_NAME, part)
+ set_state_initial(next, GdUnitType.FOLDER)
+ current_parent = next
+
+ # Move test suites to deepest folder
+ var test_suites := item.get_children()
+ for test_suite in test_suites:
+ item.remove_child(test_suite)
+ current_parent.add_child(test_suite)
+
+ # Remove the flat folder
+ item.get_parent().remove_child(item)
+ item.free()
+
+
+func flatmap_folders(parent: TreeItem) -> Dictionary:
+ var folder_map := {}
+
+ for item in parent.get_children():
+ if is_folder(item):
+ var current_path: String = item.get_meta(META_GDUNIT_NAME)
+ # Get parent folder paths
+ var parent_path := get_parent_folder_path(item)
+ if parent_path:
+ current_path = parent_path + "/" + current_path
+
+ # Collect direct children of this folder
+ var children: Array[TreeItem] = []
+ for child in item.get_children():
+ if is_test_suite(child):
+ children.append(child)
+
+ # Add children to existing path or create new entry
+ if not children.is_empty():
+ if folder_map.has(current_path):
+ @warning_ignore("unsafe_method_access")
+ folder_map[current_path].append_array(children)
+ else:
+ folder_map[current_path] = children
+
+ # Recursively process subfolders
+ var sub_folders := flatmap_folders(item)
+ for path: String in sub_folders.keys():
+ if folder_map.has(path):
+ @warning_ignore("unsafe_method_access")
+ folder_map[path].append_array(sub_folders[path])
+ else:
+ folder_map[path] = sub_folders[path]
+ return folder_map
+
+
+func get_parent_folder_path(item: TreeItem) -> String:
+ var path := ""
+ var parent := item.get_parent()
+
+ while parent != _tree_root:
+ if is_folder(parent):
+ path = parent.get_meta(META_GDUNIT_NAME) + ("/" + path if path else "")
+ parent = parent.get_parent()
+
+ return path
+
+
+func cleanup_empty_folders(parent: TreeItem) -> void:
+ var folders: Array[TreeItem] = []
+ # First collect all folders to avoid modification during iteration
+ for item in parent.get_children():
+ if is_folder(item):
+ folders.append(item)
+
+ # Process collected folders
+ for folder in folders:
+ cleanup_empty_folders(folder)
+ # Remove folder if it has no children after cleanup
+ if folder.get_child_count() == 0:
+ parent.remove_child(folder)
+ folder.free()
+
+
+func reset_tree_state(parent: TreeItem) -> void:
+ if parent == _tree_root:
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0)
+ _tree_root.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
+ test_counters_changed.emit(0, 0, STATE.INITIAL)
+
+ for item in parent.get_children():
+ set_state_initial(item, get_item_type(item))
+ reset_tree_state(item)
+
+
+func select_item(item: TreeItem) -> TreeItem:
+ if item != null:
+ # enshure the parent is collapsed
+ do_collapse_parent(item)
+ item.select(0)
+ _tree.ensure_cursor_is_visible()
+ _tree.scroll_to_item(item, true)
+ return item
+
+
+func do_collapse_parent(item: TreeItem) -> void:
+ if item != null:
+ item.collapsed = false
+ do_collapse_parent(item.get_parent())
+
+
+func do_collapse_all(collapse: bool, parent := _tree_root) -> void:
+ for item in parent.get_children():
+ item.collapsed = collapse
+ if not collapse:
+ do_collapse_all(collapse, item)
+
+
+func set_state_initial(item: TreeItem, type: GdUnitType) -> void:
+ item.set_text(0, str(item.get_meta(META_GDUNIT_NAME)))
+ item.set_custom_color(0, Color.LIGHT_GRAY)
+ item.set_tooltip_text(0, "")
+ item.set_text_overrun_behavior(0, TextServer.OVERRUN_TRIM_CHAR)
+ item.set_expand_right(0, true)
+
+ item.set_custom_color(1, Color.LIGHT_GRAY)
+ item.set_text(1, "")
+ item.set_expand_right(1, true)
+ item.set_tooltip_text(1, "")
+
+ item.set_meta(META_GDUNIT_STATE, STATE.INITIAL)
+ item.set_meta(META_GDUNIT_TYPE, type)
+ item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
+ item.set_meta(META_GDUNIT_EXECUTION_TIME, 0)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) and item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) > 0:
+ item.set_text(0, "(0/%d) %s" % [item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)])
+ item.remove_meta(META_GDUNIT_REPORT)
+ item.remove_meta(META_GDUNIT_ORPHAN)
+
+ set_item_icon_by_state(item)
+
+
+func set_state_running(item: TreeItem, is_running: bool) -> void:
+ if is_state_running(item):
+ return
+ if is_item_state(item, STATE.INITIAL):
+ item.set_custom_color(0, Color.DARK_GREEN)
+ item.set_custom_color(1, Color.DARK_GREEN)
+ item.set_meta(META_GDUNIT_STATE, STATE.RUNNING)
+ item.collapsed = false
+
+ if is_running:
+ item.set_icon(0, ICON_SPINNER)
+ else:
+ set_item_icon_by_state(item)
+ for child in item.get_children():
+ set_item_icon_by_state(child)
+
+ var parent := item.get_parent()
+ if parent != _tree_root:
+ set_state_running(parent, is_running)
+
+
+func set_state_succeded(item: TreeItem) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item) or is_state_failed(item):
+ return
+ if item == _tree_root:
+ return
+ item.set_custom_color(0, Color.GREEN)
+ item.set_custom_color(1, Color.GREEN)
+ item.set_meta(META_GDUNIT_STATE, STATE.SUCCESS)
+ item.collapsed = GdUnitSettings.is_inspector_node_collapse()
+ set_item_icon_by_state(item)
+
+
+func set_state_flaky(item: TreeItem, event: GdUnitEvent) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item):
+ return
+ var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT)
+ item.set_meta(META_GDUNIT_STATE, STATE.FLAKY)
+ if retry_count > 1:
+ var item_text: String = item.get_meta(META_GDUNIT_NAME)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]
+ item.set_text(0, "%s (%s retries)" % [item_text, retry_count])
+ item.set_custom_color(0, Color.GREEN_YELLOW)
+ item.set_custom_color(1, Color.GREEN_YELLOW)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_skipped(item: TreeItem) -> void:
+ item.set_meta(META_GDUNIT_STATE, STATE.SKIPPED)
+ item.set_text(1, "(skipped)")
+ item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
+ item.set_custom_color(0, Color.DARK_GRAY)
+ item.set_custom_color(1, Color.DARK_GRAY)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_warnings(item: TreeItem) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item) or is_state_failed(item):
+ return
+ item.set_meta(META_GDUNIT_STATE, STATE.WARNING)
+ item.set_custom_color(0, Color.YELLOW)
+ item.set_custom_color(1, Color.YELLOW)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_failed(item: TreeItem, event: GdUnitEvent) -> void:
+ # Do not overwrite higher states
+ if is_state_error(item):
+ return
+ var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT)
+ if retry_count > 1:
+ var item_text: String = item.get_meta(META_GDUNIT_NAME)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]
+ item.set_text(0, "%s (%s retries)" % [item_text, retry_count])
+ item.set_meta(META_GDUNIT_STATE, STATE.FAILED)
+ item.set_custom_color(0, Color.LIGHT_BLUE)
+ item.set_custom_color(1, Color.LIGHT_BLUE)
+ item.collapsed = false
+ set_item_icon_by_state(item)
+
+
+func set_state_error(item: TreeItem) -> void:
+ item.set_meta(META_GDUNIT_STATE, STATE.ERROR)
+ item.set_custom_color(0, Color.ORANGE_RED)
+ item.set_custom_color(1, Color.ORANGE_RED)
+ set_item_icon_by_state(item)
+ item.collapsed = false
+
+
+func set_state_aborted(item: TreeItem) -> void:
+ item.set_meta(META_GDUNIT_STATE, STATE.ABORDED)
+ item.set_custom_color(0, Color.ORANGE_RED)
+ item.set_custom_color(1, Color.ORANGE_RED)
+ item.clear_custom_bg_color(0)
+ item.set_text(1, "(aborted)")
+ item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
+ set_item_icon_by_state(item)
+ item.collapsed = false
+
+
+func set_state_orphan(item: TreeItem, event: GdUnitEvent) -> void:
+ var orphan_count := event.statistic(GdUnitEvent.ORPHAN_NODES)
+ if orphan_count == 0:
+ return
+ if item.has_meta(META_GDUNIT_ORPHAN):
+ orphan_count += item.get_meta(META_GDUNIT_ORPHAN)
+ item.set_meta(META_GDUNIT_ORPHAN, orphan_count)
+ if item.get_meta(META_GDUNIT_STATE) != STATE.FAILED:
+ item.set_custom_color(0, Color.YELLOW)
+ item.set_custom_color(1, Color.YELLOW)
+ item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count)
+ set_item_icon_by_state(item)
+
+
+func update_state(item: TreeItem, event: GdUnitEvent, add_reports := true) -> void:
+ # we do not show the root
+ if item == null:
+ return
+
+ if event.is_skipped():
+ set_state_skipped(item)
+ elif event.is_success() and event.is_flaky():
+ set_state_flaky(item, event)
+ elif event.is_success():
+ set_state_succeded(item)
+ elif event.is_error():
+ set_state_error(item)
+ elif event.is_failed():
+ set_state_failed(item, event)
+ elif event.is_warning():
+ set_state_warnings(item)
+ if add_reports:
+ for report in event.reports():
+ add_report(item, report)
+ set_state_orphan(item, event)
+
+ var parent := item.get_parent()
+ if parent == null:
+ return
+
+ var item_state: int = item.get_meta(META_GDUNIT_STATE)
+ var parent_state: int = parent.get_meta(META_GDUNIT_STATE)
+ if item_state <= parent_state:
+ return
+ update_state(item.get_parent(), event, false)
+
+
+func add_report(item: TreeItem, report: GdUnitReport) -> void:
+ var reports: Array[GdUnitReport] = []
+ if item.has_meta(META_GDUNIT_REPORT):
+ reports = get_item_reports(item)
+ reports.append(report)
+ item.set_meta(META_GDUNIT_REPORT, reports)
+
+
+func abort_running(items:=_tree_root.get_children()) -> void:
+ for item in items:
+ if item.get_icon(0) == ICON_SPINNER:
+ set_state_aborted(item)
+ abort_running(item.get_children())
+
+
+func _on_select_next_item_by_state(item_state: int) -> TreeItem:
+ var current_selected := _tree.get_selected()
+ # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found
+ current_selected = _find_first_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state)
+ # If no next failure found, then we try to select first
+ if current_selected == null:
+ current_selected = _find_first_item_by_state(_tree_root, item_state)
+ return select_item(current_selected)
+
+
+func _on_select_previous_item_by_state(item_state: int) -> TreeItem:
+ var current_selected := _tree.get_selected()
+ # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found
+ current_selected = _find_last_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state, true)
+ # If no next failure found, then we try to select first last
+ if current_selected == null:
+ current_selected = _find_last_item_by_state(_tree_root, item_state)
+ return select_item(current_selected)
+
+
+func select_first_orphan() -> void:
+ for parent in _tree_root.get_children():
+ if not is_state_success(parent):
+ for item in parent.get_children():
+ if is_item_state_orphan(item):
+ parent.set_collapsed(false)
+ @warning_ignore("return_value_discarded")
+ select_item(item)
+ return
+
+
+func clear_reports() -> void:
+ for child in _report_list.get_children():
+ _report_list.remove_child(child)
+ child.queue_free()
+
+
+func show_failed_report(selected_item: TreeItem) -> void:
+ clear_reports()
+ if selected_item == null or not selected_item.has_meta(META_GDUNIT_REPORT):
+ return
+ # add new reports
+ for report in get_item_reports(selected_item):
+ var reportNode: RichTextLabel = _report_template.duplicate()
+ _report_list.add_child(reportNode)
+ reportNode.push_color(Color.DARK_TURQUOISE)
+ reportNode.append_text(report.to_string())
+ reportNode.pop()
+ reportNode.visible = true
+
+
+func update_test_suite(event: GdUnitEvent) -> void:
+ var item := _find_tree_item_by_test_suite(_tree_root, event.resource_path(), event.suite_name())
+ if not item:
+ push_error("[InspectorTreeMainPanel#update_test_suite] Internal Error: Can't find test suite item '{_suite_name}' for {_resource_path} ".format(event))
+ return
+ if event.type() == GdUnitEvent.TESTSUITE_AFTER:
+ update_item_elapsed_time_counter(item, event.elapsed_time())
+ update_state(item, event)
+ set_state_running(item, false)
+
+
+func update_test_case(event: GdUnitEvent) -> void:
+ var item := _find_tree_item_by_id(_tree_root, event.guid())
+ if not item:
+ #push_error("Internal Error: Can't find test id %s" % [event.guid()])
+ return
+ if event.type() == GdUnitEvent.TESTCASE_BEFORE:
+ set_state_running(item, true)
+ # force scrolling to current test case
+ _tree.scroll_to_item(item, true)
+ return
+
+ if event.type() == GdUnitEvent.TESTCASE_AFTER:
+ update_item_elapsed_time_counter(item, event.elapsed_time())
+ if event.is_success() or event.is_warning():
+ update_item_processed_counter(item)
+ update_state(item, event)
+ update_progress_counters(item)
+
+
+func create_item(parent: TreeItem, test: GdUnitTestCase, item_name: String, type: GdUnitType) -> TreeItem:
+ var item := _tree.create_item(parent)
+ item.collapsed = true
+ item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index())
+ item.set_text(0, item_name)
+ match type:
+ GdUnitType.TEST_CASE:
+ item.set_meta(META_TEST_CASE, test)
+ GdUnitType.TEST_GROUP:
+ # We need to create a copy of the test record meta with a new uniqe guid
+ item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.test_name))
+ GdUnitType.TEST_SUITE:
+ # We need to create a copy of the test record meta with a new uniqe guid
+ item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.suite_name))
+
+ item.set_meta(META_GDUNIT_NAME, item_name)
+ set_state_initial(item, type)
+ update_item_total_counter(item)
+ return item
+
+
+func set_item_icon_by_state(item :TreeItem) -> void:
+ if item == _tree_root:
+ return
+ var state :STATE = item.get_meta(META_GDUNIT_STATE)
+ var is_orphan := is_item_state_orphan(item)
+ var resource_path := get_item_source_file(item)
+ item.set_icon(0, get_icon_by_file_type(resource_path, state, is_orphan))
+ if item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER:
+ item.set_icon_modulate(0, Color.SKY_BLUE)
+
+
+func update_item_total_counter(item: TreeItem) -> void:
+ if item == null:
+ return
+
+ var child_count := get_total_child_count(item)
+ if child_count > 0:
+ item.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, child_count)
+ item.set_text(0, "(0/%d) %s" % [child_count, item.get_meta(META_GDUNIT_NAME)])
+
+ update_item_total_counter(item.get_parent())
+
+
+func get_total_child_count(item: TreeItem) -> int:
+ var total_count := 0
+ for child in item.get_children():
+ total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) else 1
+ return total_count
+
+
+func update_item_processed_counter(item: TreeItem, add_count := 1) -> void:
+ if item == _tree_root:
+ return
+
+ var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + add_count
+ item.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count)
+ if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ item.set_text(0, "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)])
+
+ update_item_processed_counter(item.get_parent(), add_count)
+
+
+func update_progress_counters(item: TreeItem) -> void:
+ var index: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_INDEX) + 1
+ var total_test: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX)
+ var state: STATE = item.get_meta(META_GDUNIT_STATE)
+ test_counters_changed.emit(index, total_test, state)
+ _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, index)
+
+
+func recalculate_counters(parent: TreeItem) -> void:
+ # Reset the counter first
+ if parent.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ if parent.has_meta(META_GDUNIT_PROGRESS_INDEX):
+ parent.set_meta(META_GDUNIT_PROGRESS_INDEX, 0)
+ if parent.has_meta(META_GDUNIT_SUCCESS_TESTS):
+ parent.set_meta(META_GDUNIT_SUCCESS_TESTS, 0)
+
+ # Calculate new count based on children
+ var total_count := 0
+ var success_count := 0
+ var progress_index := 0
+
+ for child in parent.get_children():
+ if child.get_child_count() > 0:
+ # Recursively update child counters first
+ recalculate_counters(child)
+ # Add child's counters to parent
+ if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX):
+ total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX)
+ if child.has_meta(META_GDUNIT_SUCCESS_TESTS):
+ success_count += child.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ if child.has_meta(META_GDUNIT_PROGRESS_INDEX):
+ progress_index += child.get_meta(META_GDUNIT_PROGRESS_INDEX)
+ elif is_test_case(child):
+ # Count individual test cases
+ total_count += 1
+ # Count completed tests
+ if is_state_success(child) or is_state_warning(child) or is_state_failed(child) or is_state_error(child):
+ progress_index += 1
+ if is_state_success(child) or is_state_warning(child):
+ success_count += 1
+
+ # Update the counters
+ if total_count > 0:
+ parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_count)
+ parent.set_meta(META_GDUNIT_PROGRESS_INDEX, progress_index)
+ parent.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count)
+
+ # Update the display text
+ parent.set_text(0, "(%d/%d) %s" % [success_count, total_count, parent.get_meta(META_GDUNIT_NAME)])
+
+
+func update_item_elapsed_time_counter(item: TreeItem, time: int) -> void:
+ item.set_text(1, "%s" % LocalTime.elapsed(time))
+ item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
+ item.set_meta(META_GDUNIT_EXECUTION_TIME, time)
+
+ var parent := item.get_parent()
+ if parent == _tree_root:
+ return
+ var elapsed_time :int = parent.get_meta(META_GDUNIT_EXECUTION_TIME) + time
+ var type :GdUnitType = item.get_meta(META_GDUNIT_TYPE)
+ match type:
+ GdUnitType.TEST_CASE:
+ return
+ GdUnitType.TEST_SUITE:
+ update_item_elapsed_time_counter(parent, elapsed_time)
+ #GdUnitType.FOLDER:
+ # update_item_elapsed_time_counter(parent, elapsed_time)
+
+
+func get_icon_by_file_type(path: String, state: STATE, orphans: bool) -> Texture2D:
+ if path.get_extension() == "gd":
+ match state:
+ STATE.INITIAL:
+ return ICON_GDSCRIPT_TEST_DEFAULT
+ STATE.SUCCESS:
+ return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_SUCCESS
+ STATE.ERROR:
+ return ICON_GDSCRIPT_TEST_ERRORS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_ERROR
+ STATE.FAILED:
+ return ICON_GDSCRIPT_TEST_FAILED_ORPHAN if orphans else ICON_GDSCRIPT_TEST_FAILED
+ STATE.WARNING:
+ return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_DEFAULT
+ STATE.FLAKY:
+ return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_FLAKY
+ _:
+ return ICON_GDSCRIPT_TEST_DEFAULT
+ if path.get_extension() == "cs":
+ match state:
+ STATE.INITIAL:
+ return ICON_CSSCRIPT_TEST_DEFAULT
+ STATE.SUCCESS:
+ return ICON_CSSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_CSSCRIPT_TEST_SUCCESS
+ STATE.ERROR:
+ return ICON_CSSCRIPT_TEST_ERRORS_ORPHAN if orphans else ICON_CSSCRIPT_TEST_ERROR
+ STATE.FAILED:
+ return ICON_CSSCRIPT_TEST_FAILED_ORPHAN if orphans else ICON_CSSCRIPT_TEST_FAILED
+ STATE.WARNING:
+ return ICON_CSSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_CSSCRIPT_TEST_DEFAULT
+ _:
+ return ICON_CSSCRIPT_TEST_DEFAULT
+ match state:
+ STATE.INITIAL:
+ return ICON_FOLDER
+ STATE.ERROR:
+ return ICON_FOLDER
+ STATE.FAILED:
+ return ICON_FOLDER
+ _:
+ return ICON_FOLDER
+
+
+func on_test_case_discover_added(test_case: GdUnitTestCase) -> void:
+ var test_root_folder := GdUnitSettings.test_root_folder().replace("res://", "")
+ var fully_qualified_name := test_case.fully_qualified_name.trim_suffix(test_case.display_name)
+ var parts := fully_qualified_name.split(".", false)
+ parts.append(test_case.display_name)
+ # Skip tree structure until test root folder
+ var index := parts.find(test_root_folder)
+ if index != -1:
+ parts = parts.slice(index+1)
+
+ match _current_tree_view_mode:
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT:
+ create_items_tree_mode_flat(test_case, parts)
+ GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE:
+ create_items_tree_mode_tree(test_case, parts)
+
+
+func create_items_tree_mode_tree(test_case: GdUnitTestCase, parts: PackedStringArray) -> void:
+ var parent := _tree_root
+ var is_suite_assigned := false
+ var suite_name := test_case.suite_name.split(".")[-1]
+ for item_name in parts:
+ var next := _find_tree_item(parent, item_name)
+ if next != null:
+ parent = next
+ continue
+
+ if not is_suite_assigned and suite_name == item_name:
+ next = create_item(parent, test_case, item_name, GdUnitType.TEST_SUITE)
+ is_suite_assigned = true
+ elif item_name == test_case.display_name:
+ next = create_item(parent, test_case, item_name, GdUnitType.TEST_CASE)
+ # On grouped tests (parameterized tests)
+ elif item_name == test_case.test_name:
+ next = create_item(parent, test_case, item_name, GdUnitType.TEST_GROUP)
+ else:
+ next = create_item(parent, test_case, item_name, GdUnitType.FOLDER)
+ parent = next
+
+
+func create_items_tree_mode_flat(test_case: GdUnitTestCase, parts: PackedStringArray) -> void:
+ # All parts except the last two (suite name and test name/display name)
+ var slice_index := -2 if parts[-1] == test_case.test_name else -3
+ var path_parts := parts.slice(0, slice_index)
+ var folder_path := "/".join(path_parts)
+
+ # Find or create flat folder
+ var folder_item: TreeItem
+ if folder_path.is_empty():
+ folder_item = _tree_root
+ else:
+ folder_item = _find_tree_item(_tree_root, folder_path)
+ if folder_item == null:
+ folder_item = create_item(_tree_root, test_case, folder_path, GdUnitType.FOLDER)
+
+ # Find suite under the flat folder (second to last part)
+ var suite_item := _find_tree_item(folder_item, test_case.suite_name)
+ if suite_item == null:
+ suite_item = create_item(folder_item, test_case, test_case.suite_name, GdUnitType.TEST_SUITE)
+
+ # Add test case or group under the suite
+ if test_case.test_name != test_case.display_name:
+ # It's a parameterized test group
+ var group_item := _find_tree_item(suite_item, test_case.test_name)
+ if group_item == null:
+ group_item = create_item(suite_item, test_case, test_case.test_name, GdUnitType.TEST_GROUP)
+ create_item(group_item, test_case, test_case.display_name, GdUnitType.TEST_CASE)
+ else:
+ create_item(suite_item, test_case, test_case.display_name, GdUnitType.TEST_CASE)
+
+
+func on_test_case_discover_deleted(test_case: GdUnitTestCase) -> void:
+ var item := _find_tree_item_by_id(_tree_root, test_case.guid)
+ if item != null:
+ var parent := item.get_parent()
+ parent.remove_child(item)
+
+ # update the cached counters
+ var item_success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS)
+ var item_total_test_count: int = item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ var total_test_count: int = parent.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0)
+ parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_test_count-item_total_test_count)
+
+ # propagate counter update to all parents
+ update_item_total_counter(parent)
+ update_item_processed_counter(parent, -item_success_count)
+
+
+func on_test_case_discover_modified(test_case: GdUnitTestCase) -> void:
+ var item := _find_tree_item_by_id(_tree_root, test_case.guid)
+ if item != null:
+ item.set_meta(META_TEST_CASE, test_case)
+ item.set_text(0, test_case.display_name)
+ item.set_meta(META_GDUNIT_NAME, test_case.display_name)
+
+
+func get_item_reports(item: TreeItem) -> Array[GdUnitReport]:
+ return item.get_meta(META_GDUNIT_REPORT)
+
+
+func get_item_test_line_number(item: TreeItem) -> int:
+ if item == null or not item.has_meta(META_TEST_CASE):
+ return -1
+
+ var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ return test_case.line_number
+
+
+func get_item_source_file(item: TreeItem) -> String:
+ if item == null or not item.has_meta(META_TEST_CASE):
+ return ""
+
+ var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ return test_case.source_file
+
+
+func get_item_type(item: TreeItem) -> GdUnitType:
+ if item == null or not item.has_meta(META_GDUNIT_TYPE):
+ return GdUnitType.FOLDER
+ return item.get_meta(META_GDUNIT_TYPE)
+
+
+func _dump_tree_as_json(dump_name: String) -> void:
+ var dict := _to_json(_tree_root)
+ var file := FileAccess.open("res://%s.json" % dump_name, FileAccess.WRITE)
+ file.store_string(JSON.stringify(dict, "\t"))
+
+
+func _to_json(parent :TreeItem) -> Dictionary:
+ var item_as_dict := GdObjects.obj2dict(parent)
+ item_as_dict["TreeItem"]["childrens"] = parent.get_children().map(func(item: TreeItem) -> Dictionary:
+ return _to_json(item))
+ return item_as_dict
+
+
+func extract_resource_path(event: GdUnitEvent) -> String:
+ return ProjectSettings.localize_path(event.resource_path())
+
+
+func collect_test_cases(item: TreeItem, tests: Array[GdUnitTestCase] = []) -> Array[GdUnitTestCase]:
+ for next in item.get_children():
+ collect_test_cases(next, tests)
+
+ if is_test_case(item):
+ var test: GdUnitTestCase = item.get_meta(META_TEST_CASE)
+ if not tests.has(test):
+ tests.append(test)
+
+ return tests
+
+
+func test_session_start() -> void:
+ _context_menu.disable_items()
+ reset_tree_state(_tree_root)
+ clear_reports()
+
+
+func test_session_stop() -> void:
+ _context_menu.enable_items()
+ abort_running()
+ sort_tree_items(_tree_root)
+ # wait until the tree redraw
+ await get_tree().process_frame
+ var failure_item := _find_first_item_by_state(_tree_root, STATE.FAILED)
+ select_item( failure_item if failure_item else _current_selected_item)
+
+
+################################################################################
+# Tree signal receiver
+################################################################################
+func _on_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: int) -> void:
+ if mouse_button_index == MOUSE_BUTTON_RIGHT:
+ _context_menu.position = get_screen_position() + mouse_position
+ _context_menu.popup()
+
+
+func _on_Tree_item_selected() -> void:
+ _current_selected_item = _tree.get_selected()
+ show_failed_report(_current_selected_item)
+ tree_item_selected.emit(_current_selected_item)
+
+
+# Opens the test suite
+func _on_Tree_item_activated() -> void:
+ var selected_item := _tree.get_selected()
+ var line_number := get_item_test_line_number(selected_item)
+ if line_number != -1:
+ var script_path := ProjectSettings.localize_path(get_item_source_file(selected_item))
+ var resource: Script = load(script_path)
+
+ if selected_item.has_meta(META_GDUNIT_REPORT):
+ var reports := get_item_reports(selected_item)
+ var report_line_number := reports[0].line_number()
+ # if number -1 we use original stored line number of the test case
+ # in non debug mode the line number is not available
+ if report_line_number != -1:
+ line_number = report_line_number
+
+ EditorInterface.get_file_system_dock().navigate_to_path(script_path)
+ EditorInterface.edit_script(resource, line_number)
+ elif selected_item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER:
+ # Toggle collapse if dir
+ selected_item.collapsed = not selected_item.collapsed
+
+
+################################################################################
+# external signal receiver
+################################################################################
+
+func _on_gdunit_event(event: GdUnitEvent) -> void:
+ match event.type():
+ GdUnitEvent.DISCOVER_START:
+ _tree_root.visible = false
+ _discover_hint.visible = true
+ init_tree()
+
+ GdUnitEvent.DISCOVER_END:
+ sort_tree_items(_tree_root)
+ select_item(_tree_root.get_first_child())
+ _discover_hint.visible = false
+ _tree_root.visible = true
+ #_dump_tree_as_json("tree_example_discovered")
+
+ GdUnitEvent.TESTCASE_BEFORE:
+ update_test_case(event)
+
+ GdUnitEvent.TESTCASE_AFTER:
+ update_test_case(event)
+
+ GdUnitEvent.TESTSUITE_BEFORE:
+ update_test_suite(event)
+
+ GdUnitEvent.TESTSUITE_AFTER:
+ update_test_suite(event)
+
+ GdUnitEvent.SESSION_START:
+ test_session_start()
+
+ GdUnitEvent.SESSION_CLOSE:
+ await test_session_stop()
+
+
+func _on_settings_changed(property :GdUnitProperty) -> void:
+ match property.name():
+ GdUnitSettings.INSPECTOR_TREE_SORT_MODE:
+ sort_tree_items(_tree_root)
+ #_dump_tree_as_json("tree_sorted_by_%s" % GdUnitInspectorTreeConstants.SORT_MODE.keys()[property.value()])
+
+ GdUnitSettings.INSPECTOR_TREE_VIEW_MODE:
+ restructure_tree(_tree_root, GdUnitSettings.get_inspector_tree_view_mode())