ajhahn.de
← Theria
GDScript 222 lines
# ------------------------------------------------------------------------------
# It keeps track of the orphans...so this is best name it could ever have.
# ------------------------------------------------------------------------------
class Orphanage:
	const UNGROUPED = "Outside Tests"
	const SUBGROUP_SEP = '->'

	var orphan_ids = {}
	var oprhans_by_group = {}
	var strutils = GutUtils.Strutils.new()

	# wrapper for stubbing
	func _get_system_orphan_node_ids():
		return Node.get_orphan_node_ids()


	func _make_group_key(group=null, subgroup=null):
		var to_return = UNGROUPED
		if(group != null):
			to_return = group

		if(subgroup == null):
			to_return += str(SUBGROUP_SEP, UNGROUPED)
		else:
			to_return += str(SUBGROUP_SEP, subgroup)

		return to_return


	func _add_orphan_by_group(id, group, subgroup):
		var key = _make_group_key(group, subgroup)
		if(oprhans_by_group.has(key)):
			oprhans_by_group[key].append(id)
		else:
			oprhans_by_group[key] = [id]


	func process_orphans(group=null, subgroup=null):
		var new_orphans = []
		for orphan_id in _get_system_orphan_node_ids():
			if(!orphan_ids.has(orphan_id)):
				new_orphans.append(orphan_id)
				orphan_ids[orphan_id] = {
					"group":GutUtils.nvl(group, UNGROUPED),
					"subgroup":GutUtils.nvl(subgroup, UNGROUPED),
					"instance":instance_from_id(orphan_id)
				}
				_add_orphan_by_group(orphan_id, group, subgroup)

		return new_orphans


	func get_orphan_ids(group=null, subgroup=null):
		var key = _make_group_key(group, subgroup)
		return oprhans_by_group.get(key, [])


	# Given the likely size, this was way easier than making a dictionary
	# of dictionaries of arrays.
	func get_all_group_orphans(group):
		var to_return = []
		for key in oprhans_by_group:
			if(key == group or key.begins_with(str(group, SUBGROUP_SEP))):
				to_return.append_array(oprhans_by_group[key])
		return to_return


	# clears out anything that is not still an orphan.
	func clean():
		oprhans_by_group.clear()
		for key in orphan_ids.keys():
			var inst = orphan_ids[key].instance
			if(!is_instance_valid(inst) or inst.get_parent() != null and not orphan_ids.has(inst.get_parent().get_instance_id())):
				orphan_ids.erase(key)
			else:
				_add_orphan_by_group(key, orphan_ids[key].group, orphan_ids[key].subgroup)




# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
var _strutils = GutStringUtils.new()

var orphanage : Orphanage = Orphanage.new()
var logger = GutUtils.get_logger()
var autofree = GutUtils.AutoFree.new()


func _count_all_children(instance):
	var count = instance.get_child_count()
	for child in instance.get_children():
		count += _count_all_children(child)
	return count


func get_orphan_list_text(orphan_ids):
	var text = ""
	for id in orphan_ids:
		var kid_count_text = ''
		var inst = orphanage.orphan_ids[id].instance
		if(is_instance_valid(inst) and inst.get_parent() == null):
			var kid_count = _count_all_children(inst)
			if(kid_count != 0):
				kid_count_text = str(' + ', kid_count)

			var autofree_text = ''
			if(autofree.has_instance_id(id)):
				autofree_text = (" (autofree)")

			if(text != ''):
				text += "\n"
			text += str('* [', _strutils.type2str(inst), ']', kid_count_text, autofree_text)

	return text


func orphan_count() -> int:
	return int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT))


func record_orphans(group, subgroup = null):
	return orphanage.process_orphans(group, subgroup)


func convert_instance_ids_to_valid_instances(instance_ids):
	var to_return = []
	for entry in instance_ids:
		if(is_instance_id_valid(entry)):
			to_return.append(instance_from_id(entry))
	return to_return


func end_script(script_path, should_log):
	record_orphans(script_path)
	var orphans = orphanage.get_all_group_orphans(script_path)
	if(orphans.size() > 0 and should_log):
		logger.orphan(str(orphans.size(), ' orphans'))


func end_test(script_path, test_name, should_log = true):
	record_orphans(script_path, test_name)
	orphanage.clean()
	# Must get all the orphans and not just the results of record_orphans
	# because record_orphans may have been called for this group/subgroup
	# already.
	var orphans = get_orphan_ids(script_path, test_name)
	if(orphans.size() > 0 and should_log):
		logger.orphan(str(orphans.size(), ' Orphans'))
		logger.inc_indent()
		logger.orphan(get_orphan_list_text(orphans))
		logger.dec_indent()


func get_orphan_ids(group=null, subgroup=null):
	var ids = []
	if(group == null):
		ids = orphanage.orphan_ids.keys()
	elif(subgroup == null):
		ids = orphanage.get_all_group_orphans(group)
	else:
		ids = orphanage.get_orphan_ids(group, subgroup)

	return ids


func get_count() -> int:
	return orphan_count()


func log_all():
	var last_script = ''
	var last_test = ''

	for id in orphanage.orphan_ids:
		var entry = orphanage.orphan_ids[id]

		if(last_script != entry.group):
			last_script = entry.group
			last_test = ''
			logger.log(entry.group)

		if(last_test != entry.subgroup):
			logger.inc_indent()
			logger.log(str('- ', entry.subgroup))
			last_test = entry.subgroup
			logger.inc_indent()
			var orphan_ids = orphanage.get_orphan_ids(last_script, last_test)
			logger.orphan(get_orphan_list_text(orphan_ids))
			logger.dec_indent()
			logger.dec_indent()


# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################