@tool class_name GutUtils extends Object const GUT_METADATA = '__gutdbl' # Note, these cannot change since places are checking for TYPE_INT to determine # how to process parameters. enum DOUBLE_STRATEGY{ INCLUDE_NATIVE, SCRIPT_ONLY, } enum DIFF { DEEP, SIMPLE } const TEST_STATUSES = { NO_ASSERTS = 'no asserts', SKIPPED = 'skipped', NOT_RUN = 'not run', PENDING = 'pending', # These two got the "ed" b/c pass is a reserved word and I could not # think of better words. FAILED = 'fail', PASSED = 'pass' } const DOUBLE_TEMPLATES = { FUNCTION = 'res://addons/gut/double_templates/function_template.txt', INIT = 'res://addons/gut/double_templates/init_template.txt', SCRIPT = 'res://addons/gut/double_templates/script_template.txt', } static var GutScene = load('res://addons/gut/GutScene.tscn') static var LazyLoader = load('res://addons/gut/lazy_loader.gd') static var VersionNumbers = load("res://addons/gut/version_numbers.gd") static var WarningsManager = load("res://addons/gut/warnings_manager.gd") # -------------------------------- # Lazy loaded scripts. These scripts are lazy loaded so that they can be # declared, but will not load when this script is loaded. This gives us a # window at the start of a run to adjust warning levels prior to loading # everything. # -------------------------------- static var AutoFree = LazyLoader.new('res://addons/gut/autofree.gd'): get: return AutoFree.get_loaded() set(val): pass static var Awaiter = LazyLoader.new('res://addons/gut/awaiter.gd'): get: return Awaiter.get_loaded() set(val): pass static var Comparator = LazyLoader.new('res://addons/gut/comparator.gd'): get: return Comparator.get_loaded() set(val): pass static var CollectedTest = LazyLoader.new('res://addons/gut/collected_test.gd'): get: return CollectedTest.get_loaded() set(val): pass static var CollectedScript = LazyLoader.new('res://addons/gut/collected_script.gd'): get: return CollectedScript.get_loaded() set(val): pass static var CompareResult = LazyLoader.new('res://addons/gut/compare_result.gd'): get: return CompareResult.get_loaded() set(val): pass static var DiffFormatter = LazyLoader.new("res://addons/gut/diff_formatter.gd"): get: return DiffFormatter.get_loaded() set(val): pass static var DiffTool = LazyLoader.new('res://addons/gut/diff_tool.gd'): get: return DiffTool.get_loaded() set(val): pass static var DoubleTools = LazyLoader.new("res://addons/gut/double_tools.gd"): get: return DoubleTools.get_loader() set(val): pass static var Doubler = LazyLoader.new('res://addons/gut/doubler.gd'): get: return Doubler.get_loaded() set(val): pass static var DynamicGdScript = LazyLoader.new("res://addons/gut/dynamic_gdscript.gd") : get: return DynamicGdScript.get_loaded() set(val): pass static var Gut = LazyLoader.new('res://addons/gut/gut.gd'): get: return Gut.get_loaded() set(val): pass static var GutConfig = LazyLoader.new('res://addons/gut/gut_config.gd'): get: return GutConfig.get_loaded() set(val): pass static var HookScript = LazyLoader.new('res://addons/gut/hook_script.gd'): get: return HookScript.get_loaded() set(val): pass static var InnerClassRegistry = LazyLoader.new('res://addons/gut/inner_class_registry.gd'): get: return InnerClassRegistry.get_loaded() set(val): pass static var InputFactory = LazyLoader.new("res://addons/gut/input_factory.gd"): get: return InputFactory.get_loaded() set(val): pass static var InputSender = LazyLoader.new("res://addons/gut/input_sender.gd"): get: return InputSender.get_loaded() set(val): pass static var JunitXmlExport = LazyLoader.new('res://addons/gut/junit_xml_export.gd'): get: return JunitXmlExport.get_loaded() set(val): pass static var Logger = LazyLoader.new('res://addons/gut/logger.gd') : # everything should use get_logger get: return Logger.get_loaded() set(val): pass static var MethodMaker = LazyLoader.new('res://addons/gut/method_maker.gd'): get: return MethodMaker.get_loaded() set(val): pass static var OneToMany = LazyLoader.new('res://addons/gut/one_to_many.gd'): get: return OneToMany.get_loaded() set(val): pass static var OrphanCounter = LazyLoader.new('res://addons/gut/orphan_counter.gd'): get: return OrphanCounter.get_loaded() set(val): pass static var ParameterFactory = LazyLoader.new('res://addons/gut/parameter_factory.gd'): get: return ParameterFactory.get_loaded() set(val): pass static var ParameterHandler = LazyLoader.new('res://addons/gut/parameter_handler.gd'): get: return ParameterHandler.get_loaded() set(val): pass static var Printers = LazyLoader.new('res://addons/gut/printers.gd'): get: return Printers.get_loaded() set(val): pass static var ResultExporter = LazyLoader.new('res://addons/gut/result_exporter.gd'): get: return ResultExporter.get_loaded() set(val): pass static var ScriptCollector = LazyLoader.new('res://addons/gut/script_parser.gd'): get: return ScriptCollector.get_loaded() set(val): pass static var SignalWatcher = LazyLoader.new('res://addons/gut/signal_watcher.gd'): get: return SignalWatcher.get_loaded() set(val): pass static var Spy = LazyLoader.new('res://addons/gut/spy.gd'): get: return Spy.get_loaded() set(val): pass static var Strutils = LazyLoader.new('res://addons/gut/strutils.gd'): get: return Strutils.get_loaded() set(val): pass static var Stubber = LazyLoader.new('res://addons/gut/stubber.gd'): get: return Stubber.get_loaded() set(val): pass static var StubParams = LazyLoader.new('res://addons/gut/stub_params.gd'): get: return StubParams.get_loaded() set(val): pass static var Summary = LazyLoader.new('res://addons/gut/summary.gd'): get: return Summary.get_loaded() set(val): pass static var Test = LazyLoader.new('res://addons/gut/test.gd'): get: return Test.get_loaded() set(val): pass static var TestCollector = LazyLoader.new('res://addons/gut/test_collector.gd'): get: return TestCollector.get_loaded() set(val): pass static var ThingCounter = LazyLoader.new('res://addons/gut/thing_counter.gd'): get: return ThingCounter.get_loaded() set(val): pass # -------------------------------- static var avail_fonts = ['AnonymousPro', 'CourierPrime', 'LobsterTwo', 'Default'] static var version_numbers = VersionNumbers.new( # gut_versrion (source of truth) '9.3.1', # required_godot_version '4.2.0' ) static var warnings_at_start := { # WarningsManager dictionary exclude_addons = true } static var warnings_when_loading_test_scripts := { # WarningsManager dictionary enable = false } # ------------------------------------------------------------------------------ # Everything should get a logger through this. # # When running in test mode this will always return a new logger so that errors # are not caused by getting bad warn/error/etc counts. # ------------------------------------------------------------------------------ static var _test_mode = false static var _lgr = null static func get_logger(): if(_test_mode): return Logger.new() else: if(_lgr == null): _lgr = Logger.new() return _lgr static var _dyn_gdscript = DynamicGdScript.new() static func create_script_from_source(source, override_path=null): var are_warnings_enabled = WarningsManager.are_warnings_enabled() WarningsManager.enable_warnings(false) var DynamicScript = _dyn_gdscript.create_script_from_source(source, override_path) if(typeof(DynamicScript) == TYPE_INT): var l = get_logger() l.error(str('Could not create script from source. Error: ', DynamicScript)) l.info(str("Source Code:\n", add_line_numbers(source))) WarningsManager.enable_warnings(are_warnings_enabled) return DynamicScript static func godot_version_string(): return version_numbers.make_godot_version_string() static func is_godot_version(expected): return VersionNumbers.VerNumTools.is_godot_version_eq(expected) static func is_godot_version_gte(expected): return VersionNumbers.VerNumTools.is_godot_version_gte(expected) const INSTALL_OK_TEXT = 'Everything checks out' static func make_install_check_text(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): var text = INSTALL_OK_TEXT if(!FileAccess.file_exists(template_paths.FUNCTION) or !FileAccess.file_exists(template_paths.INIT) or !FileAccess.file_exists(template_paths.SCRIPT)): text = 'One or more GUT template files are missing. If this is an exported project, you must include *.txt files in the export to run GUT. If it is not an exported project then reinstall GUT.' elif(!ver_nums.is_godot_version_valid()): text = ver_nums.get_bad_version_text() return text static func is_install_valid(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): return make_install_check_text(template_paths, ver_nums) == INSTALL_OK_TEXT # ------------------------------------------------------------------------------ # Gets the root node without having to be in the tree and pushing out an error # if we don't have a main loop ready to go yet. # ------------------------------------------------------------------------------ # static func get_root_node(): # var main_loop = Engine.get_main_loop() # if(main_loop != null): # return main_loop.root # else: # push_error('No Main Loop Yet') # return null # ------------------------------------------------------------------------------ # Gets the value from an enum. # - If passed an integer value as a string it will convert it to an int and # processes the int value. # - If the value is a float then it is converted to an int and then processes # the int value # - If the value is an int, or was converted to an int, then the enum is checked # to see if it contains the value, if so then the value is returned. # Otherwise the default is returned. # - If the value is a string then it is uppercased and all spaces are replaced # with underscores. It then checks to see if enum contains a key of that # name. If so then the value for that key is returned, otherwise the default # is returned. # # This description is longer than the code, you should have just read the code # and the tests. # ------------------------------------------------------------------------------ static func get_enum_value(thing, e, default=null): var to_return = default if(typeof(thing) == TYPE_STRING and str(thing.to_int()) == thing): thing = thing.to_int() elif(typeof(thing) == TYPE_FLOAT): thing = int(thing) if(typeof(thing) == TYPE_STRING): var converted = thing.to_upper().replace(' ', '_') if(e.keys().has(converted)): to_return = e[converted] else: if(e.values().has(thing)): to_return = thing return to_return # ------------------------------------------------------------------------------ # return if_null if value is null otherwise return value # ------------------------------------------------------------------------------ static func nvl(value, if_null): if(value == null): return if_null else: return value # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ static func pretty_print(dict, indent = ' '): print(JSON.stringify(dict, indent)) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ static func print_properties(props, thing, print_all_meta=false): for i in range(props.size()): var prop_name = props[i].name var prop_value = thing.get(props[i].name) var print_value = str(prop_value) if(print_value.length() > 100): print_value = print_value.substr(0, 97) + '...' elif(print_value == ''): print_value = 'EMPTY' print(prop_name, ' = ', print_value) if(print_all_meta): print(' ', props[i]) # ------------------------------------------------------------------------------ # Gets the value of the node_property 'script' from a PackedScene's root node. # This does not assume the location of the root node in the PackedScene's node # list. This also does not assume the index of the 'script' node property in # a nodes's property list. # ------------------------------------------------------------------------------ static func get_scene_script_object(scene): var state = scene.get_state() var to_return = null var root_node_path = NodePath(".") var node_idx = 0 while(node_idx < state.get_node_count() and to_return == null): if(state.get_node_path(node_idx) == root_node_path): for i in range(state.get_node_property_count(node_idx)): if(state.get_node_property_name(node_idx, i) == 'script'): to_return = state.get_node_property_value(node_idx, i) node_idx += 1 return to_return # ------------------------------------------------------------------------------ # returns true if the object has been freed, false if not # # From what i've read, the weakref approach should work. It seems to work most # of the time but sometimes it does not catch it. The str comparison seems to # fill in the gaps. I've not seen any errors after adding that check. # ------------------------------------------------------------------------------ static func is_freed(obj): var wr = weakref(obj) return !(wr.get_ref() and str(obj) != '') # ------------------------------------------------------------------------------ # Pretty self explanitory. # ------------------------------------------------------------------------------ static func is_not_freed(obj): return !is_freed(obj) # ------------------------------------------------------------------------------ # Checks if the passed in object is a GUT Double or Partial Double. # ------------------------------------------------------------------------------ static func is_double(obj): var to_return = false if(typeof(obj) == TYPE_OBJECT and is_instance_valid(obj)): to_return = obj.has_method('__gutdbl_check_method__') return to_return # ------------------------------------------------------------------------------ # Checks an object to see if it is a GDScriptNativeClass # ------------------------------------------------------------------------------ static func is_native_class(thing): var it_is = false if(typeof(thing) == TYPE_OBJECT): it_is = str(thing).begins_with("= 0): temp = decimal_value >> count if(temp & 1): binary_string = binary_string + "1" else: binary_string = binary_string + "0" count -= 1 return binary_string static func add_line_numbers(contents): if(contents == null): return '' var to_return = "" var lines = contents.split("\n") var line_num = 1 for line in lines: var line_str = str(line_num).lpad(6, ' ') to_return += str(line_str, ' |', line, "\n") line_num += 1 return to_return static func get_display_size(): return Engine.get_main_loop().get_viewport().get_visible_rect() # ############################################################################## #(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. # # ##############################################################################