summaryrefslogtreecommitdiff
path: root/addons/gut/doubler.gd
diff options
context:
space:
mode:
authorSophia Pearson <codergal89@gmail.com>2022-05-20 00:45:25 +0200
committerSophia Pearson <codergal89@gmail.com>2022-05-20 18:56:04 +0200
commit05d29ccce1898ed89c0b650c77242c2fa2805128 (patch)
treee8ee3bcb570fa6f3d9d96273c2bf4d4c8618d08b /addons/gut/doubler.gd
downloadtexty-05d29ccce1898ed89c0b650c77242c2fa2805128.tar.xz
texty-05d29ccce1898ed89c0b650c77242c2fa2805128.zip
texty: initial commit
Diffstat (limited to 'addons/gut/doubler.gd')
-rw-r--r--addons/gut/doubler.gd760
1 files changed, 760 insertions, 0 deletions
diff --git a/addons/gut/doubler.gd b/addons/gut/doubler.gd
new file mode 100644
index 0000000..78becdd
--- /dev/null
+++ b/addons/gut/doubler.gd
@@ -0,0 +1,760 @@
+# ##############################################################################
+#(G)odot (U)nit (T)est class
+#
+# ##############################################################################
+# The MIT License (MIT)
+# =====================
+#
+# Copyright (c) 2020 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.
+#
+# ##############################################################################
+# Description
+# -----------
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# Utility class to hold the local and built in methods separately. Add all local
+# methods FIRST, then add built ins.
+# ------------------------------------------------------------------------------
+class ScriptMethods:
+ # List of methods that should not be overloaded when they are not defined
+ # in the class being doubled. These either break things if they are
+ # overloaded or do not have a "super" equivalent so we can't just pass
+ # through.
+ var _blacklist = [
+ 'has_method',
+ 'get_script',
+ 'get',
+ '_notification',
+ 'get_path',
+ '_enter_tree',
+ '_exit_tree',
+ '_process',
+ '_draw',
+ '_physics_process',
+ '_input',
+ '_unhandled_input',
+ '_unhandled_key_input',
+ '_set',
+ '_get', # probably
+ 'emit_signal', # can't handle extra parameters to be sent with signal.
+ 'draw_mesh', # issue with one parameter, value is `Null((..), (..), (..))``
+ '_to_string', # nonexistant function ._to_string
+ '_get_minimum_size', # Nonexistent function _get_minimum_size
+ ]
+
+ # These methods should not be included in the double.
+ var _skip = [
+ # There is an init in the template. There is also no real reason
+ # to include this method since it will always be called, it has no
+ # return value, and you cannot prevent super from being called.
+ '_init'
+ ]
+
+ var built_ins = []
+ var local_methods = []
+ var _method_names = []
+
+ func is_blacklisted(method_meta):
+ return _blacklist.find(method_meta.name) != -1
+
+ func _add_name_if_does_not_have(method_name):
+ if(_skip.has(method_name)):
+ return false
+ var should_add = _method_names.find(method_name) == -1
+ if(should_add):
+ _method_names.append(method_name)
+ return should_add
+
+ func add_built_in_method(method_meta):
+ var did_add = _add_name_if_does_not_have(method_meta.name)
+ if(did_add and !is_blacklisted(method_meta)):
+ built_ins.append(method_meta)
+
+ func add_local_method(method_meta):
+ var did_add = _add_name_if_does_not_have(method_meta.name)
+ if(did_add):
+ local_methods.append(method_meta)
+
+ func to_s():
+ var text = "Locals\n"
+ for i in range(local_methods.size()):
+ text += str(" ", local_methods[i].name, "\n")
+ text += "Built-Ins\n"
+ for i in range(built_ins.size()):
+ text += str(" ", built_ins[i].name, "\n")
+ return text
+
+# ------------------------------------------------------------------------------
+# Helper class to deal with objects and inner classes.
+# ------------------------------------------------------------------------------
+class ObjectInfo:
+ var _path = null
+ var _subpaths = []
+ var _utils = load('res://addons/gut/utils.gd').get_instance()
+ var _lgr = _utils.get_logger()
+ var _method_strategy = null
+ var make_partial_double = false
+ var scene_path = null
+ var _native_class = null
+ var _native_class_name = null
+ var _singleton_instance = null
+ var _singleton_name = null
+
+ func _init(path, subpath=null):
+ _path = path
+ if(subpath != null):
+ _subpaths = Array(subpath.split('/'))
+
+ # Returns an instance of the class/inner class
+ func instantiate():
+ var to_return = null
+
+ if(_singleton_instance != null):
+ to_return = _singleton_instance
+ elif(is_native()):
+ to_return = _native_class.new()
+ else:
+ to_return = get_loaded_class().new()
+
+ return to_return
+
+
+ # Can't call it get_class because that is reserved so it gets this ugly name.
+ # Loads up the class and then any inner classes to give back a reference to
+ # the desired Inner class (if there is any)
+ func get_loaded_class():
+ var LoadedClass = load(_path)
+ for i in range(_subpaths.size()):
+ LoadedClass = LoadedClass.get(_subpaths[i])
+ return LoadedClass
+
+
+ func to_s():
+ return str(_path, '[', get_subpath(), ']')
+
+
+ func get_path():
+ return _path
+
+
+ func get_subpath():
+ return PoolStringArray(_subpaths).join('/')
+
+
+ func has_subpath():
+ return _subpaths.size() != 0
+
+
+ func get_method_strategy():
+ return _method_strategy
+
+
+ func set_method_strategy(method_strategy):
+ _method_strategy = method_strategy
+
+
+ func is_native():
+ return _native_class != null
+
+
+ func set_native_class(native_class):
+ _native_class = native_class
+ var inst = native_class.new()
+ _native_class_name = inst.get_class()
+ _path = _native_class_name
+ if(!inst is Reference):
+ inst.free()
+
+
+ func get_native_class_name():
+ return _native_class_name
+
+
+ func get_singleton_instance():
+ return _singleton_instance
+
+
+ func get_singleton_name():
+ return _singleton_name
+
+
+ func set_singleton_name(singleton_name):
+ _singleton_name = singleton_name
+ _singleton_instance = _utils.get_singleton_by_name(_singleton_name)
+
+
+ func is_singleton():
+ return _singleton_instance != null
+
+
+ func get_extends_text():
+ var extend = null
+ if(is_singleton()):
+ extend = str("# Double of singleton ", _singleton_name, ", base class is Reference")
+ elif(is_native()):
+ var native = get_native_class_name()
+ if(native.begins_with('_')):
+ native = native.substr(1)
+ extend = str("extends ", native)
+ else:
+ extend = str("extends '", get_path(), "'")
+
+ if(has_subpath()):
+ extend += str('.', get_subpath().replace('/', '.'))
+
+ return extend
+
+
+ func get_constants_text():
+ if(!is_singleton()):
+ return ""
+
+ # do not include constants defined in the super class which for
+ # singletons stubs is Reference.
+ var exclude_constants = Array(ClassDB.class_get_integer_constant_list("Reference"))
+ var text = str("# -----\n# ", _singleton_name, " Constants\n# -----\n")
+ var constants = ClassDB.class_get_integer_constant_list(_singleton_name)
+ for c in constants:
+ if(!exclude_constants.has(c)):
+ var value = ClassDB.class_get_integer_constant(_singleton_name, c)
+ text += str("const ", c, " = ", value, "\n")
+
+ return text
+
+ func get_properties_text():
+ if(!is_singleton()):
+ return ""
+
+ var text = str("# -----\n# ", _singleton_name, " Properties\n# -----\n")
+ var props = ClassDB.class_get_property_list(_singleton_name)
+ for prop in props:
+ var accessors = {"setter":null, "getter":null}
+ var prop_text = str("var ", prop["name"])
+
+ var getter_name = "get_" + prop["name"]
+ if(ClassDB.class_has_method(_singleton_name, getter_name)):
+ accessors.getter = getter_name
+ else:
+ getter_name = "is_" + prop["name"]
+ if(ClassDB.class_has_method(_singleton_name, getter_name)):
+ accessors.getter = getter_name
+
+ var setter_name = "set_" + prop["name"]
+ if(ClassDB.class_has_method(_singleton_name, setter_name)):
+ accessors.setter = setter_name
+
+ var setget_text = ""
+ if(accessors.setter != null and accessors.getter != null):
+ setget_text = str("setget ", accessors.setter, ", ", accessors.getter)
+ else:
+ # never seen this message show up, but it should show up if we
+ # get misbehaving singleton.
+ _lgr.error(str("Could not find setget methods for property: ",
+ _singleton_name, ".", prop["name"]))
+
+ text += str(prop_text, " ", setget_text, "\n")
+
+ return text
+
+
+# ------------------------------------------------------------------------------
+# Allows for interacting with a file but only creating a string. This was done
+# to ease the transition from files being created for doubles to loading
+# doubles from a string. This allows the files to be created for debugging
+# purposes since reading a file is easier than reading a dumped out string.
+# ------------------------------------------------------------------------------
+class FileOrString:
+ extends File
+
+ var _do_file = false
+ var _contents = ''
+ var _path = null
+
+ func open(path, mode):
+ _path = path
+ if(_do_file):
+ return .open(path, mode)
+ else:
+ return OK
+
+ func close():
+ if(_do_file):
+ return .close()
+
+ func store_string(s):
+ if(_do_file):
+ .store_string(s)
+ _contents += s
+
+ func get_contents():
+ return _contents
+
+ func get_path():
+ return _path
+
+ func load_it():
+ if(_contents != ''):
+ var script = GDScript.new()
+ script.set_source_code(get_contents())
+ script.reload()
+ return script
+ else:
+ return load(_path)
+
+# ------------------------------------------------------------------------------
+# A stroke of genius if I do say so. This allows for doubling a scene without
+# having to write any files. By overloading the "instance" method we can
+# make whatever we want.
+# ------------------------------------------------------------------------------
+class PackedSceneDouble:
+ extends PackedScene
+ var _script = null
+ var _scene = null
+
+ func set_script_obj(obj):
+ _script = obj
+
+ func instance(edit_state=0):
+ var inst = _scene.instance(edit_state)
+ if(_script != null):
+ inst.set_script(_script)
+ return inst
+
+ func load_scene(path):
+ _scene = load(path)
+
+
+
+
+# ------------------------------------------------------------------------------
+# START Doubler
+# ------------------------------------------------------------------------------
+var _utils = load('res://addons/gut/utils.gd').get_instance()
+
+var _ignored_methods = _utils.OneToMany.new()
+var _stubber = _utils.Stubber.new()
+var _lgr = _utils.get_logger()
+var _method_maker = _utils.MethodMaker.new()
+
+var _output_dir = 'user://gut_temp_directory'
+var _double_count = 0 # used in making files names unique
+var _spy = null
+var _gut = null
+var _strategy = null
+var _base_script_text = _utils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')
+var _make_files = false
+# used by tests for debugging purposes.
+var _print_source = false
+
+func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL):
+ set_logger(_utils.get_logger())
+ _strategy = strategy
+
+# ###############
+# Private
+# ###############
+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(obj_info, method_name):
+ if(_utils.non_super_methods.has(method_name)):
+ return
+
+ var path = obj_info.get_path()
+ if(obj_info.is_singleton()):
+ path = obj_info.get_singleton_name()
+ elif(obj_info.scene_path != null):
+ path = obj_info.scene_path
+
+ var params = _utils.StubParams.new(path, method_name, obj_info.get_subpath())
+ params.to_call_super()
+ _stubber.add_stub(params)
+
+
+func _get_base_script_text(obj_info, override_path):
+ var path = obj_info.get_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 values = {
+ # Top sections
+ "extends":obj_info.get_extends_text(),
+ "constants":obj_info.get_constants_text(),
+ "properties":obj_info.get_properties_text(),
+
+ # metadata values
+ "path":path,
+ "subpath":obj_info.get_subpath(),
+ "stubber_id":stubber_id,
+ "spy_id":spy_id,
+ "gut_id":gut_id,
+ "singleton_name":_utils.nvl(obj_info.get_singleton_name(), ''),
+ "is_partial":str(obj_info.make_partial_double).to_lower()
+ }
+
+ return _base_script_text.format(values)
+
+
+func _write_file(obj_info, dest_path, override_path=null):
+ var base_script = _get_base_script_text(obj_info, override_path)
+ var script_methods = _get_methods(obj_info)
+ var super_name = ""
+ var path = ""
+
+ if(obj_info.is_singleton()):
+ super_name = obj_info.get_singleton_name()
+ else:
+ path = obj_info.get_path()
+
+ var f = FileOrString.new()
+ f._do_file = _make_files
+ var f_result = f.open(dest_path, f.WRITE)
+
+ if(f_result != OK):
+ _lgr.error(str('Error creating file ', dest_path))
+ _lgr.error(str('Could not create double for :', obj_info.to_s()))
+ return
+
+ f.store_string(base_script)
+
+ for i in range(script_methods.local_methods.size()):
+ f.store_string(_get_func_text(script_methods.local_methods[i], path, super_name))
+
+ for i in range(script_methods.built_ins.size()):
+ _stub_to_call_super(obj_info, script_methods.built_ins[i].name)
+ f.store_string(_get_func_text(script_methods.built_ins[i], path, super_name))
+
+ f.close()
+ if(_print_source):
+ print(f.get_contents())
+ return f
+
+
+func _double_scene_and_script(scene_info):
+ var to_return = PackedSceneDouble.new()
+ to_return.load_scene(scene_info.get_path())
+
+ var inst = load(scene_info.get_path()).instance()
+ var script_path = null
+ if(inst.get_script()):
+ script_path = inst.get_script().get_path()
+ inst.free()
+
+ if(script_path):
+ var oi = ObjectInfo.new(script_path)
+ oi.set_method_strategy(scene_info.get_method_strategy())
+ oi.make_partial_double = scene_info.make_partial_double
+ oi.scene_path = scene_info.get_path()
+ to_return.set_script_obj(_double(oi, scene_info.get_path()).load_it())
+
+ return to_return
+
+
+func _get_methods(object_info):
+ var obj = object_info.instantiate()
+ # any method in the script or super script
+ var script_methods = ScriptMethods.new()
+ var methods = obj.get_method_list()
+
+ if(!object_info.is_singleton() and !(obj is Reference)):
+ obj.free()
+
+ # first pass is for local methods only
+ for i in range(methods.size()):
+ if(object_info.is_singleton()):
+ #print(methods[i].name, " :: ", methods[i].flags, " :: ", methods[i].id)
+ #print(" ", methods[i])
+
+ # It appears that the ID for methods upstream from a singleton are
+ # below 200. Initially it was thought that singleton specific methods
+ # were above 1000. This was true for Input but not for OS. I've
+ # changed the condition to be > 200 instead of > 1000. It will take
+ # some investigation to figure out if this is right, but it works
+ # for now. Someone either find an issue and open a bug, or this will
+ # just exist like this. Sorry future me (or someone else).
+ if(methods[i].id > 200 and methods[i].flags in [1, 9]):
+ script_methods.add_local_method(methods[i])
+
+ # 65 is a magic number for methods in script, though documentation
+ # says 64. This picks up local overloads of base class methods too.
+ # See MethodFlags in @GlobalScope
+ elif(methods[i].flags == 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
+ script_methods.add_local_method(methods[i])
+
+ if(object_info.get_method_strategy() == _utils.DOUBLE_STRATEGY.FULL):
+ # second pass is for anything not local
+ for j in range(methods.size()):
+ # 65 is a magic number for methods in script, though documentation
+ # says 64. This picks up local overloads of base class methods too.
+ if(methods[j].flags != 65 and !_ignored_methods.has(object_info.get_path(), methods[j]['name'])):
+ script_methods.add_built_in_method(methods[j])
+
+ return script_methods
+
+
+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, path, super=""):
+ var override_count = null;
+ if(_stubber != null):
+ override_count = _stubber.get_parameter_count(path, method_hash.name)
+
+ var text = _method_maker.get_function_text(method_hash, path, override_count, super) + "\n"
+
+ return text
+
+# returns the path to write the double file to
+func _get_temp_path(object_info):
+ var file_name = null
+ var extension = null
+
+ if(object_info.is_singleton()):
+ file_name = str(object_info.get_singleton_instance())
+ extension = "gd"
+ elif(object_info.is_native()):
+ file_name = object_info.get_native_class_name()
+ extension = 'gd'
+ else:
+ file_name = object_info.get_path().get_file().get_basename()
+ extension = object_info.get_path().get_extension()
+
+ if(object_info.has_subpath()):
+ file_name += '__' + object_info.get_subpath().replace('/', '__')
+
+ file_name += str('__dbl', _double_count, '__.', extension)
+
+ var to_return = _output_dir.plus_file(file_name)
+ return to_return
+
+
+func _double(obj_info, override_path=null):
+ var temp_path = _get_temp_path(obj_info)
+ var result = _write_file(obj_info, temp_path, override_path)
+ _double_count += 1
+ return result
+
+
+func _double_script(path, make_partial, strategy):
+ var oi = ObjectInfo.new(path)
+ oi.make_partial_double = make_partial
+ oi.set_method_strategy(strategy)
+ return _double(oi).load_it()
+
+
+func _double_inner(path, subpath, make_partial, strategy):
+ var oi = ObjectInfo.new(path, subpath)
+ oi.set_method_strategy(strategy)
+ oi.make_partial_double = make_partial
+ return _double(oi).load_it()
+
+
+func _double_scene(path, make_partial, strategy):
+ var oi = ObjectInfo.new(path)
+ oi.set_method_strategy(strategy)
+ oi.make_partial_double = make_partial
+ return _double_scene_and_script(oi)
+
+
+func _double_gdnative(native_class, make_partial, strategy):
+ var oi = ObjectInfo.new(null)
+ oi.set_native_class(native_class)
+ oi.set_method_strategy(strategy)
+ oi.make_partial_double = make_partial
+ return _double(oi).load_it()
+
+
+func _double_singleton(singleton_name, make_partial, strategy):
+ var oi = ObjectInfo.new(null)
+ oi.set_singleton_name(singleton_name)
+ oi.set_method_strategy(_utils.DOUBLE_STRATEGY.PARTIAL)
+ oi.make_partial_double = make_partial
+ return _double(oi).load_it()
+
+# ###############
+# Public
+# ###############
+func get_output_dir():
+ return _output_dir
+
+
+func set_output_dir(output_dir):
+ if(output_dir != null):
+ _output_dir = output_dir
+ if(_make_files):
+ var d = Directory.new()
+ d.make_dir_recursive(output_dir)
+
+
+func get_spy():
+ return _spy
+
+
+func set_spy(spy):
+ _spy = spy
+
+
+func get_stubber():
+ return _stubber
+
+
+func set_stubber(stubber):
+ _stubber = stubber
+
+
+func get_logger():
+ return _lgr
+
+
+func set_logger(logger):
+ _lgr = logger
+ _method_maker.set_logger(logger)
+
+
+func get_strategy():
+ return _strategy
+
+
+func set_strategy(strategy):
+ _strategy = strategy
+
+
+func get_gut():
+ return _gut
+
+
+func set_gut(gut):
+ _gut = gut
+
+
+func partial_double_scene(path, strategy=_strategy):
+ return _double_scene(path, true, strategy)
+
+
+# double a scene
+func double_scene(path, strategy=_strategy):
+ return _double_scene(path, false, strategy)
+
+
+# double a script/object
+func double(path, strategy=_strategy):
+ return _double_script(path, false, strategy)
+
+
+func partial_double(path, strategy=_strategy):
+ return _double_script(path, true, strategy)
+
+
+func partial_double_inner(path, subpath, strategy=_strategy):
+ return _double_inner(path, subpath, true, strategy)
+
+
+# double an inner class in a script
+func double_inner(path, subpath, strategy=_strategy):
+ return _double_inner(path, subpath, false, strategy)
+
+
+# must always use FULL strategy since this is a native class and you won't get
+# any methods if you don't use FULL
+func double_gdnative(native_class):
+ return _double_gdnative(native_class, false, _utils.DOUBLE_STRATEGY.FULL)
+
+
+# must always use FULL strategy since this is a native class and you won't get
+# any methods if you don't use FULL
+func partial_double_gdnative(native_class):
+ return _double_gdnative(native_class, true, _utils.DOUBLE_STRATEGY.FULL)
+
+
+func double_singleton(name):
+ return _double_singleton(name, false, _utils.DOUBLE_STRATEGY.PARTIAL)
+
+
+func partial_double_singleton(name):
+ return _double_singleton(name, true, _utils.DOUBLE_STRATEGY.PARTIAL)
+
+
+func clear_output_directory():
+ if(!_make_files):
+ return false
+
+ var did = false
+ if(_output_dir.find('user://') == 0):
+ var d = Directory.new()
+ var result = d.open(_output_dir)
+ # BIG GOTCHA HERE. If it cannot open the dir w/ erro 31, then the
+ # directory becomes res:// and things go on normally and gut clears out
+ # out res:// which is SUPER BAD.
+ if(result == OK):
+ d.list_dir_begin(true)
+ var f = d.get_next()
+ while(f != ''):
+ d.remove(f)
+ f = d.get_next()
+ did = true
+ return did
+
+func delete_output_directory():
+ var did = clear_output_directory()
+ if(did):
+ var d = Directory.new()
+ d.remove(_output_dir)
+
+
+func add_ignored_method(path, method_name):
+ _ignored_methods.add(path, method_name)
+
+
+func get_ignored_methods():
+ return _ignored_methods
+
+
+func get_make_files():
+ return _make_files
+
+
+func set_make_files(make_files):
+ _make_files = make_files
+ set_output_dir(_output_dir)
+
+func get_method_maker():
+ return _method_maker \ No newline at end of file