Files
Cozypaw-Hospital/addons/gut/doubler.gd
Steven Wroblewski c8fb2d959f chore(tooling): install GUT v9.6.0 test framework
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:33:12 +02:00

396 lines
11 KiB
GDScript

extends RefCounted
static var _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')
static var _singleton_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/singleton_template.txt')
static var _double_data_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/double_data_template.txt')
var _script_collector = GutUtils.ScriptCollector.new()
var _singleton_parser = GutUtils.SingletonParser.new()
# used by tests for debugging purposes.
var print_source = false
var inner_class_registry = GutUtils.InnerClassRegistry.new()
# ###############
# Properties
# ###############
var _stubber = GutUtils.Stubber.new()
func get_stubber():
return _stubber
func set_stubber(stubber):
_stubber = stubber
var _lgr = GutUtils.get_logger()
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
_method_maker.set_logger(logger)
var _spy = null
func get_spy():
return _spy
func set_spy(spy):
_spy = spy
var _gut = null
func get_gut():
return _gut
func set_gut(gut):
_gut = gut
var _strategy = null
func get_strategy():
return _strategy
func set_strategy(strategy):
if(GutUtils.DOUBLE_STRATEGY.values().has(strategy)):
_strategy = strategy
else:
_lgr.error(str('doubler.gd: invalid double strategy ', strategy))
var _method_maker = GutUtils.MethodMaker.new()
func get_method_maker():
return _method_maker
var _ignored_methods = GutUtils.OneToMany.new()
func get_ignored_methods():
return _ignored_methods
# ###############
# Private
# ###############
func _init(strategy=GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY):
set_logger(GutUtils.get_logger())
_strategy = strategy
func _notification(what: int) -> void:
if(what == NOTIFICATION_PREDELETE):
if(_stubber != null):
_stubber.clear()
func _get_indented_line(indents, text):
var to_return = ''
for _i in range(indents):
to_return += "\t"
return str(to_return, text, "\n")
func _stub_to_call_super(parsed, method_name):
if(!parsed.get_method(method_name).is_eligible_for_doubling()):
return
var params = null
if(parsed.is_native):
params = GutUtils.StubParams.new(parsed._native_class, method_name, parsed.subpath)
else:
params = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath)
params.to_call_super()
params.is_script_default = true
_stubber.add_stub(params)
func _get_base_script_text(parsed, override_path, partial, included_methods):
var path = parsed.script_path
if(override_path != null):
path = override_path
var stubber_id = -1
if(_stubber != null):
stubber_id = _stubber.get_instance_id()
var spy_id = -1
if(_spy != null):
spy_id = _spy.get_instance_id()
var gut_id = -1
if(_gut != null):
gut_id = _gut.get_instance_id()
var extends_text = parsed.get_extends_text()
var double_data_values = {
"path":path,
"subpath":GutUtils.nvl(parsed.subpath, ''),
"stubber_id":stubber_id,
"spy_id":spy_id,
"gut_id":gut_id,
"singleton_name":'',
"singleton_id":-1,
"is_partial":partial,
"doubled_methods":included_methods,
}
var values = {
"extends":extends_text,
"double_data":_double_data_text.format(double_data_values),
}
return _base_script_text.format(values)
func _get_singleton_text(parsed, included_methods, is_partial):
var stubber_id = -1
if(_stubber != null):
stubber_id = _stubber.get_instance_id()
var spy_id = -1
if(_spy != null):
spy_id = _spy.get_instance_id()
var gut_id = -1
if(_gut != null):
gut_id = _gut.get_instance_id()
var double_data_values = {
"path":'',
"subpath":'',
"stubber_id":stubber_id,
"spy_id":spy_id,
"gut_id":gut_id,
"singleton_name":parsed.singleton_name,
"singleton_id":parsed.singleton_id,
"is_partial":is_partial,
"doubled_methods":included_methods,
}
var values = {
"extends":"extends RefCounted",
"double_data":_double_data_text.format(double_data_values),
"signals":parsed.get_all_signal_text(),
"constants":parsed.get_all_constants_text(),
"properties":parsed.get_all_properties_text()
}
var src = _singleton_script_text.format(values)
return src
func _is_method_eligible_for_doubling(parsed_script, parsed_method):
return !parsed_method.is_accessor() and \
parsed_method.is_eligible_for_doubling() and \
!_ignored_methods.has(parsed_script.resource, parsed_method.meta.name)
# Disable the native_method_override setting so that doubles do not generate
# errors or warnings when doubling with INCLUDE_NATIVE or when a method has
# been added because of param_count stub.
func _create_script_no_warnings(src):
var prev_native_override_value = null
var native_method_override = 'debug/gdscript/warnings/native_method_override'
prev_native_override_value = ProjectSettings.get_setting(native_method_override)
ProjectSettings.set_setting(native_method_override, 0)
var DblClass = GutUtils.create_script_from_source(src)
ProjectSettings.set_setting(native_method_override, prev_native_override_value)
return DblClass
func _create_double(parsed, strategy, override_path, partial):
var dbl_src = ""
var included_methods = []
for method in parsed.get_local_methods():
if(_is_method_eligible_for_doubling(parsed, method)):
included_methods.append(method.meta.name)
dbl_src += _get_func_text(method.meta)
if(strategy == GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE):
for method in parsed.get_super_methods():
if(_is_method_eligible_for_doubling(parsed, method)):
included_methods.append(method.meta.name)
_stub_to_call_super(parsed, method.meta.name)
dbl_src += _get_func_text(method.meta)
var base_script = _get_base_script_text(parsed, override_path, partial, included_methods)
dbl_src = base_script + "\n\n" + dbl_src
if(print_source):
var to_print :String = GutUtils.add_line_numbers(dbl_src)
to_print = to_print.rstrip("\n")
_lgr.log(str(to_print))
var DblClass = _create_script_no_warnings(dbl_src)
if(_stubber != null):
_stub_method_default_values(parsed)
if(print_source):
_lgr.log(str(" path | ", DblClass.resource_path, "\n"))
return DblClass
func _create_singleton_double(singleton, is_partial):
var parsed = _singleton_parser.parse(singleton)
var dbl_src = _get_singleton_text(parsed, parsed.methods_by_name.keys(), is_partial)
for key in parsed.methods_by_name:
if(!_ignored_methods.has(singleton, key)):
dbl_src += _method_maker.get_function_text(parsed.methods_by_name[key], singleton) + "\n"
if(print_source):
var to_print :String = GutUtils.add_line_numbers(dbl_src)
to_print = to_print.rstrip("\n")
_lgr.log(str(to_print))
var DblClass = GutUtils.create_script_from_source(dbl_src)
if(_stubber != null):
for key in parsed.methods_by_name:
var meta = parsed.methods_by_name[key]
if(meta != {} and !meta.flags & METHOD_FLAG_VARARG):
_stubber.stub_defaults_from_meta(singleton, meta)
return DblClass
func _stub_method_default_values(parsed):
for method in parsed.get_local_methods():
if(method.is_eligible_for_doubling() and !_ignored_methods.has(parsed.resource, method.meta.name)):
_stubber.stub_defaults_from_meta(parsed.script_path, method.meta)
func _double_scene_and_script(scene, strategy, partial):
var dbl_bundle = scene._bundled.duplicate(true)
var script_obj = GutUtils.get_scene_script_object(scene)
# I'm not sure if the script object for the root node of a packed scene is
# always the first entry in "variants" so this tries to find it.
var script_index = dbl_bundle["variants"].find(script_obj)
var script_dbl = null
if(script_obj != null):
if(partial):
script_dbl = _partial_double(script_obj, strategy, scene.get_path())
else:
script_dbl = _double(script_obj, strategy, scene.get_path())
if(script_index != -1):
dbl_bundle["variants"][script_index] = script_dbl
var doubled_scene = PackedScene.new()
doubled_scene._set_bundled_scene(dbl_bundle)
return doubled_scene
func _get_inst_id_ref_str(inst):
var ref_str = 'null'
if(inst):
ref_str = str('instance_from_id(', inst.get_instance_id(),')')
return ref_str
func _get_func_text(method_hash):
return _method_maker.get_function_text(method_hash) + "\n"
func _parse_script(obj):
var parsed = null
if(GutUtils.is_inner_class(obj)):
if(inner_class_registry.has(obj)):
parsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj)
else:
_lgr.error('Doubling Inner Classes requires you register them first. Call register_inner_classes passing the script that contains the inner class.')
else:
parsed = _script_collector.parse(obj)
return parsed
# Override path is used with scenes.
func _double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, false)
func _partial_double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, true)
# -------------------------
# Public
# -------------------------
# double a script/object
func double(obj, strategy=_strategy):
return _double(obj, strategy)
func partial_double(obj, strategy=_strategy):
return _partial_double(obj, strategy)
# double a scene
func double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, false)
func partial_double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, true)
func double_gdnative(which):
return _double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func partial_double_gdnative(which):
return _partial_double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, false)
func partial_double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, true)
func double_singleton(obj):
return _create_singleton_double(obj, false)
func partial_double_singleton(obj):
return _create_singleton_double(obj, true)
func add_ignored_method(obj, method_name):
_ignored_methods.add(obj, method_name)
# ##############################################################################
#(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.
#
# ##############################################################################