diff --git a/.gutconfig.json b/.gutconfig.json index 22202ad1..bb849c53 100644 --- a/.gutconfig.json +++ b/.gutconfig.json @@ -1,8 +1,10 @@ { "dirs":["res://test/unit/", "res://test/integration/"], - "should_maximize":false, + "should_maximize":true, "should_exit":true, "ignore_pause":true, "log": 2, - "opacity":100 + "opacity":100, + "inner_class":"", + "double_strategy":"partial" } diff --git a/CHANGES.md b/CHANGES.md index b7756a25..f232e41e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,30 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +# 6.6.2 +## Features +* `assert_null` and `assert_not_null` +* added `simulate` to the test object so you no longer have to use `gut` when calling it. It is fine if you still do though. Sorry if this breaks something. +* Doubling, Stubbing, and Spies are no longer considered experimental. +* The `double` method is now smart: + * It knows when you pass it a scene or a script, no need for `double_scene` anymore but it remains. + * You can pass it a loaded class or scene like this: + ``` + var MyClass = load('res://my_class.gd') + var MyClassDouble = double(MyClass) + ``` +* You can now `double` Inner Classes by passing an Inner Class path like so: + ``` + double('res://my_script.gd', 'Inner1/InnerInInner1/AndSoOn') + ``` +* The start of an internal Gut logger to make better messages down the road. +* Experimental Full doubling allows you to Spy on most Built-in methods. You still cannot Stub them though. This must be enabled, details in the Double Wiki page. +* Added link to [Rainware's setup tutorial on youtube](https://www.youtube.com/watch?v=vBbqlfmcAlc) to the README + +## Fixes +* __Issue 94__ Gut runs `after_all` on inner classes that are skipped. +* Rawsyntax fixed a bunch of misspellings so that we can erock on with less bad speligs fo wrds. + # 6.6.1 * __Issue 60__: Improved signal assert failure messages in some cases by having them include a list of signals that were emitted for the object. @@ -9,7 +33,6 @@ This project adheres to [Semantic Versioning](http://semver.org/). * __Issue 95__: Fixed issue where sometimes Gut can end up clearing files from `res:\\` when using doubling. # 6.6.0 - ## Fixes * __Issue 79__: Scaling wasn't being accounted for by the `maximize` method. * __Issue 80__: Inner classes are no longer included in the count of scripts that were ran. @@ -125,7 +148,7 @@ assert_has_method # 6.1.0 * Moved as many files as I could to `gut_tests_and_examples` so that there was less stuff to uncheck when installing via the in-engine Asset Library. I'm still not 100% happy with the setup. -* Moved the License to `addons/gut/` so that it is distributed with the addon and doesn't accidently get copied into the root of some other project when installed via the Asset Library. +* Moved the License to `addons/gut/` so that it is distributed with the addon and doesn't accidentally get copied into the root of some other project when installed via the Asset Library. * Some README tweaks. * Fixed resize window handle bug. It was connecting to wrong signals and didn't work. * Missed changing `simulate` to call `_physics_process` instead of `_fixed_process` in the 3.0 conversion. Fixed that. diff --git a/README.md b/README.md index 97fdb8b0..e3e8fea8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# [The readme has moved to a wiki.](https://github.com/bitwes/Gut/wiki) +# [See the Wiki for Details](https://github.com/bitwes/Gut/wiki) -# 6.1.0 has a potentially bad bug, please install 6.1.1 +# 6.6.0 has a potentially bad bug, please install 6.6.2 6.1.0 has a bug that can, if everything goes wrong just right, delete files in the root of the project. I only saw it happen when running the test suite for Gut and only the `test_doubler.gd` test script. I don't recall ever seeing it happen in my own game, but just to be safe you should upgrade. ### Godot 3.1 -I've started a [3.1 branch](https://github.com/bitwes/Gut/tree/godot_3_1) that I will be keeping inline with master. I've gotten it to run but there are issues. Check open issues and the [3.1 wiki page](https://github.com/bitwes/Gut/wiki/Godot-3.1-Alpha). +I've started a [3.1 branch](https://github.com/bitwes/Gut/tree/godot_3_1) that I will be keeping inline with master. Check open issues (they will have the 3.1 tag) and the [3.1 wiki page](https://github.com/bitwes/Gut/wiki/Godot-3.1-Alpha) for any known issues. -# Gut 6.6.1 +# Gut 6.6.2 GUT (Godot Unit Test) is a utility for writing tests for your Godot Engine game. It allows you to write tests for your gdscript in gdscript. More info can be found in the [wiki](https://github.com/bitwes/Gut/wiki). @@ -18,9 +18,11 @@ Version 6.0.0 is Godot 3.0 compatible. These changes are not compatible with an Gut is provided under the MIT license. License is in `addons/gut/LICENSE.md` # Getting Started -Here's a couple [wiki](https://github.com/bitwes/Gut/wiki) links to get you started. +Here's a short setup tutorial provided by Rainware https://www.youtube.com/watch?v=vBbqlfmcAlc + +Here's a couple more [wiki](https://github.com/bitwes/Gut/wiki) links to get you started. * [Install](https://github.com/bitwes/Gut/wiki/Install) * [Creating Tests](https://github.com/bitwes/Gut/wiki/Creating-Tests) * [Methods](https://github.com/bitwes/Gut/wiki/Methods) -# [The readme has moved to a wiki.](https://github.com/bitwes/Gut/wiki) +# [See the Wiki for Details](https://github.com/bitwes/Gut/wiki) diff --git a/addons/gut/doubler.gd b/addons/gut/doubler.gd index b40695d4..98aedc20 100644 --- a/addons/gut/doubler.gd +++ b/addons/gut/doubler.gd @@ -1,25 +1,150 @@ +# ------------------------------------------------------------------------------ +# Utility class to hold the local and built in methods seperately. 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. + ] + + 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): + 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 + + +class ObjectInfo: + var _path = null + var _subpaths = [] + var _utils = load('res://addons/gut/utils.gd').new() + + func _init(path, subpath=null): + _path = path + if(subpath != null): + _subpaths = _utils.split_string(subpath, '/') + + # Returns an instance of the class/inner class + func instantiate(): + return get_loaded_class().new() + + # 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 _utils.join_array(_subpaths, '/') + + func has_subpath(): + return _subpaths.size() != 0 + + func get_extends_text(): + var extend = str("extends '", get_path(), '\'') + if(has_subpath()): + extend += str('.', get_subpath().replace('/', '.')) + return extend + + +# ------------------------------------------------------------------------------ +# START Doubler +# ------------------------------------------------------------------------------ var _output_dir = null var _stubber = null var _double_count = 0 var _use_unique_names = true var _spy = null +var _lgr = null +var _utils = load('res://addons/gut/utils.gd').new() +var _method_maker = load('res://addons/gut/method_maker.gd').new() +var _strategy = null + +func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL): + # make sure _method_maker gets logger too + set_logger(_utils.get_logger()) + _strategy = strategy # ############### # Private # ############### -func _write_file(target_path, dest_path, override_path=null): - var script_methods = _get_methods(target_path) +func _get_indented_line(indents, text): + var to_return = '' + for i in range(indents): + to_return += "\t" + return str(to_return, text, "\n") + +func _write_file(obj_info, dest_path, override_path=null): + var script_methods = _get_methods(obj_info) - var metadata = _get_stubber_metadata_text(target_path) + var metadata = _get_stubber_metadata_text(obj_info) if(override_path): - metadata = _get_stubber_metadata_text(override_path) + metadata = _get_stubber_metadata_text(obj_info, override_path) var f = File.new() f.open(dest_path, f.WRITE) - f.store_string(str("extends '", target_path, "'\n")) + + + f.store_string(str(obj_info.get_extends_text(), "\n")) f.store_string(metadata) - for i in range(script_methods.size()): - f.store_string(_get_func_text(script_methods[i])) + for i in range(script_methods.local_methods.size()): + f.store_string(_get_func_text(script_methods.local_methods[i])) + for i in range(script_methods.built_ins.size()): + f.store_string(_get_super_func_text(script_methods.built_ins[i])) f.close() func _double_scene_and_script(target_path, dest_path): @@ -33,7 +158,8 @@ func _double_scene_and_script(target_path, dest_path): inst.free() if(script_path): - var double_path = _double(script_path, target_path) + var oi = ObjectInfo.new(script_path) + var double_path = _double(oi, target_path) var dq = '"' var f = File.new() f.open(dest_path, f.READ) @@ -48,22 +174,26 @@ func _double_scene_and_script(target_path, dest_path): return script_path -func _get_methods(target_path): - var obj = load(target_path).new() +func _get_methods(object_info): + var obj = object_info.instantiate() # any mehtod in the script or super script - var script_methods = [] - # hold just the names so we can avoid duplicates. - var method_names = [] - + var script_methods = ScriptMethods.new() var methods = obj.get_method_list() + # first pass is for local mehtods only for i 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[i]['flags'] == 65): - if(!method_names.has(methods[i]['name'])): - method_names.append(methods[i]['name']) - script_methods.append(methods[i]) + if(methods[i].flags == 65): + script_methods.add_local_method(methods[i]) + + if(_strategy == _utils.DOUBLE_STRATEGY.FULL): + # second pass is for anything not local + for i 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[i].flags != 65): + script_methods.add_built_in_method(methods[i]) return script_methods @@ -73,57 +203,68 @@ func _get_inst_id_ref_str(inst): ref_str = str('instance_from_id(', inst.get_instance_id(),')') return ref_str -func _get_stubber_metadata_text(target_path): +func _get_stubber_metadata_text(obj_info, override_path = null): + var path = obj_info.get_path() + if(override_path != null): + path = override_path return "var __gut_metadata_ = {\n" + \ - "\tpath='" + target_path + "',\n" + \ + "\tpath='" + path + "',\n" + \ + "\tsubpath='" + obj_info.get_subpath() + "',\n" + \ "\tstubber=" + _get_inst_id_ref_str(_stubber) + ",\n" + \ "\tspy=" + _get_inst_id_ref_str(_spy) + "\n" + \ "}\n" -func _get_callback_parameters(method_hash): - var called_with = 'null' - if(method_hash['args'].size() > 0): - called_with = '[' - for i in range(method_hash['args'].size()): - called_with += method_hash['args'][i]['name'] - if(i < method_hash['args'].size() - 1): - called_with += ', ' - called_with += ']' - return called_with +func _get_spy_text(method_hash): + var txt = '' + if(_spy): + var called_with = _method_maker.get_spy_call_parameters_text(method_hash) + txt += "\t__gut_metadata_.spy.add_call(self, '" + method_hash.name + "', " + called_with + ")\n" + return txt func _get_func_text(method_hash): - var ftxt = str('func ', method_hash['name'], '(') - ftxt += str(_get_arg_text(method_hash['args']), "):\n") + var ftxt = _method_maker.get_decleration_text(method_hash) + "\n" + + var called_with = _method_maker.get_spy_call_parameters_text(method_hash) + ftxt += _get_spy_text(method_hash) - var called_with = _get_callback_parameters(method_hash) - if(_spy): - ftxt += "\t__gut_metadata_.spy.add_call(self, '" + method_hash['name'] + "', " + called_with + ")\n" if(_stubber): - ftxt += "\treturn __gut_metadata_.stubber.get_return(self, '" + method_hash['name'] + "', " + called_with + ")\n" + ftxt += "\treturn __gut_metadata_.stubber.get_return(self, '" + method_hash.name + "', " + called_with + ")\n" else: ftxt += "\tpass\n" return ftxt -func _get_arg_text(args): - var text = '' - for i in range(args.size()): - text += args[i]['name'] + ' = null' - if(i != args.size() -1): - text += ', ' - return text +func _get_super_func_text(method_hash): + var call_method = _method_maker.get_super_call_text(method_hash) + + var call_super_text = str("return ", call_method, "\n") + + var ftxt = _method_maker.get_decleration_text(method_hash) + "\n" + ftxt += _get_spy_text(method_hash) + + ftxt += _get_indented_line(1, call_super_text) + + return ftxt + +# returns the path to write the double file to +func _get_temp_path(object_info): + var file_name = object_info.get_path().get_file().get_basename() + var extension = object_info.get_path().get_extension() + + if(object_info.has_subpath()): + file_name += '__' + object_info.get_subpath().replace('/', '__') -func _get_temp_path(path): - var file_name = path.get_file() if(_use_unique_names): - file_name = file_name.get_basename() + \ - str('__dbl', _double_count, '__.') + file_name.get_extension() + file_name += str('__dbl', _double_count, '__.', extension) + else: + file_name += '.' + extension + var to_return = _output_dir.plus_file(file_name) return to_return -func _double(obj, override_path=null): - var temp_path = _get_temp_path(obj) - _write_file(obj, temp_path, override_path) +func _double(obj_info, override_path=null): + var temp_path = _get_temp_path(obj_info) + _write_file(obj_info, temp_path, override_path) _double_count += 1 return temp_path @@ -150,13 +291,46 @@ func get_stubber(): func set_stubber(stubber): _stubber = stubber -func double_scene(path): - var temp_path = _get_temp_path(path) +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 + +# double a scene +func double_scene(path, strategy=_strategy): + var oi = ObjectInfo.new(path) + var old_strat = _strategy + _strategy = strategy + var temp_path = _get_temp_path(oi) _double_scene_and_script(path, temp_path) + _strategy = old_strat return load(temp_path) -func double(path): - return load(_double(path)) +# double a script +func double(path, strategy=_strategy): + var oi = ObjectInfo.new(path) + var old_strat = _strategy + _strategy = strategy + var to_return = load(_double(oi)) + _strategy = old_strat + return to_return + +# double an inner class in a script +func double_inner(path, subpath, strategy=_strategy): + var oi = ObjectInfo.new(path, subpath) + var old_strat = _strategy + _strategy = strategy + var to_return = load(_double(oi)) + _strategy = old_strat + return to_return func clear_output_directory(): var did = false @@ -171,7 +345,6 @@ func clear_output_directory(): var files = [] var f = d.get_next() while(f != ''): - print('deleting ', f) d.remove(f) f = d.get_next() did = true @@ -183,5 +356,11 @@ func delete_output_directory(): var d = Directory.new() d.remove(_output_dir) +# When creating doubles a unique name is used that each double can be its own +# thing. Sometimes, for testing, we do not want to do this so this allows +# you to turn off creating unique names for each double class. +# +# THIS SHOULD NEVER BE USED OUTSIDE OF INTERNAL GUT TESTING. It can cause +# weird, hard to track down problems. func set_use_unique_names(should): _use_unique_names = should diff --git a/addons/gut/gut.gd b/addons/gut/gut.gd index bb76b049..f1261816 100644 --- a/addons/gut/gut.gd +++ b/addons/gut/gut.gd @@ -5,7 +5,7 @@ #The MIT License (MIT) #===================== # -#Copyright (c) 2017 Tom "Butch" Wesley +#Copyright (c) 2019 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 @@ -28,10 +28,13 @@ ################################################################################ # View readme for usage details. # -# Version 6.6.1 +# Version 6.6.2 ################################################################################ extends "res://addons/gut/gut_gui.gd" +var _utils = load('res://addons/gut/utils.gd').new() +var _lgr = _utils.get_logger() + # ########################### # Editor Variables # ########################### @@ -53,7 +56,7 @@ export var _file_extension = '.gd' export var _inner_class_prefix = 'Test' export(String) var _temp_directory = 'user://gut_temp_directory' export var _include_subdirectories = false setget set_include_subdirectories, get_include_subdirectories - +export(int, 'FULL', 'PARTIAL') var _double_strategy = _utils.DOUBLE_STRATEGY.PARTIAL setget set_double_strategy, get_double_strategy # Allow user to add test directories via editor. This is done with strings # instead of an array because the interface for editing arrays is really @@ -117,12 +120,13 @@ var _yielding_to = { obj = null, signal_name = '' } -var _stubber = load('res://addons/gut/stubber.gd').new() -var _doubler = load('res://addons/gut/doubler.gd').new() -var _spy = load('res://addons/gut/spy.gd').new() + +var _stubber = _utils.Stubber.new() +var _doubler = _utils.Doubler.new() +var _spy = _utils.Spy.new() const SIGNAL_TESTS_FINISHED = 'tests_finished' -const SIGNAL_STOP_YIELD_BEFORE_TEARDOWN = 'stop_yeild_before_teardown' +const SIGNAL_STOP_YIELD_BEFORE_TEARDOWN = 'stop_yield_before_teardown' # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ @@ -133,6 +137,10 @@ func _init(): _doubler.set_output_dir(_temp_directory) _doubler.set_stubber(_stubber) _doubler.set_spy(_spy) + _doubler.set_logger(_lgr) + + #_stubber.set_logger(_lgr) + #_spy.set_logger(_lgr) # ------------------------------------------------------------------------------ # Connect all the controls created in the parent class to the methods here. @@ -154,6 +162,7 @@ func _connect_controls(): # Initialize controls # ------------------------------------------------------------------------------ func _ready(): + _lgr.info(str('using [', OS.get_user_data_dir(), '] for temporary output.')) set_it_up() set_process_input(true) _connect_controls() @@ -172,7 +181,7 @@ func _ready(): # This timer is started, but it should never finish. Used # to determine how long it took to run the tests since - # getting the time and doing time math is rediculous in godot. + # getting the time and doing time math is ridiculous in godot. add_child(_runtime_timer) _runtime_timer.set_one_shot(true) _runtime_timer.set_wait_time(RUNTIME_START_TIME) @@ -424,8 +433,12 @@ func _should_yield_now(): _yield_between.tests_since_last_yield += 1 return should +# ------------------------------------------------------------------------------ +# Yes if the class name is null or the script's class name includes class_name +# ------------------------------------------------------------------------------ func _does_class_name_match(class_name, script_class_name): return class_name == null or (script_class_name != null and script_class_name.find(class_name) != -1) + # ------------------------------------------------------------------------------ # Run all tests in a script. This is the core logic for running tests. # @@ -435,7 +448,10 @@ func _does_class_name_match(class_name, script_class_name): func _test_the_scripts(): _init_run() var file = File.new() + if(_doubler.get_strategy() == _utils.DOUBLE_STRATEGY.FULL): + _lgr.info("Using Double Strategy FULL as default strategy. Keep an eye out for weirdness, this is still experimental.") + # loop through scripts for s in range(_test_collector.scripts.size()): var the_script = _test_collector.scripts[s] if(the_script.tests.size() > 0): @@ -448,10 +464,7 @@ func _test_the_scripts(): add_child(test_script) _test_script_objects.append(test_script) var script_result = null - - # call both pre-all-tests methods until prerun_setup is removed - test_script.prerun_setup() - test_script.before_all() + _doubler.set_strategy(_double_strategy) # yield between test scripts so things paint if(_yield_between.should): @@ -466,8 +479,14 @@ func _test_the_scripts(): # !!! if(!_does_class_name_match(_inner_class_name, the_script.class_name)): the_script.tests = [] + else: + # call both pre-all-tests methods until prerun_setup is removed + test_script.prerun_setup() + test_script.before_all() _ctrls.test_progress.set_max(the_script.tests.size()) + + # Each test in the script for i in range(the_script.tests.size()): _stubber.clear() _spy.clear() @@ -530,8 +549,9 @@ func _test_the_scripts(): _ctrls.test_progress.set_value(i + 1) # call both post-all-tests methods until postrun_teardown is removed. - test_script.postrun_teardown() - test_script.after_all() + if(_does_class_name_match(_inner_class_name, the_script.class_name)): + test_script.postrun_teardown() + test_script.after_all() # This might end up being very resource intensive if the scripts # don't clean up after themselves. Might have to consolidate output @@ -590,7 +610,7 @@ func p(text, level=0, indent=0): if(level <= _log_level): if(_current_test != null): - #make sure everyting printed during the execution + #make sure everything printed during the execution #of a test is at least indented once under the test if(indent == 0): indent = 1 @@ -703,7 +723,7 @@ func add_directory(path, prefix=_file_prefix, suffix=_file_extension): # ------------------------------------------------------------------------------ # This will try to find a script in the list of scripts to test that contains # the specified script name. It does not have to be a full match. It will -# select the first matching occurance so that this script will run when run_tests +# select the first matching occurrence so that this script will run when run_tests # is called. Works the same as the select_this_one option of add_script. # # returns whether it found a match or not @@ -883,7 +903,7 @@ func simulate(obj, times, delta): func set_yield_time(time, text=''): _yield_timer.set_wait_time(time) _yield_timer.start() - var msg = '/# Yeilding (' + str(time) + 's)' + var msg = '/# Yielding (' + str(time) + 's)' if(text == ''): msg += ' #/' else: @@ -930,8 +950,9 @@ func file_touch(path): # ------------------------------------------------------------------------------ func file_delete(path): var d = Directory.new() - d.open(path.get_base_dir()) - d.remove(path) + var result = d.open(path.get_base_dir()) + if(result == OK): + d.remove(path) # ------------------------------------------------------------------------------ # Checks to see if the passed in file has any data in it. @@ -957,7 +978,11 @@ func get_file_as_text(path): # ------------------------------------------------------------------------------ func directory_delete_files(path): var d = Directory.new() - d.open(path) + var result = d.open(path) + + # SHORTCIRCUIT + if(result != OK): + return # Traversing a directory is kinda odd. You have to start the process of listing # the contents of a directory with list_dir_begin then use get_next until it @@ -1022,11 +1047,32 @@ func set_inner_class_name(inner_class_name): func get_summary(): return _new_summary +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_double_strategy(): + return _double_strategy + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func set_double_strategy(double_strategy): + _double_strategy = double_strategy + _doubler.set_strategy(double_strategy) + # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ func get_include_subdirectories(): return _include_subdirectories +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_logger(): + return _lgr + +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func set_logger(logger): + _lgr = logger + # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ func set_include_subdirectories(include_subdirectories): diff --git a/addons/gut/gut_cmdln.gd b/addons/gut/gut_cmdln.gd index 58d57939..e23a26c9 100644 --- a/addons/gut/gut_cmdln.gd +++ b/addons/gut/gut_cmdln.gd @@ -5,7 +5,7 @@ #The MIT License (MIT) #===================== # -#Copyright (c) 2017 Tom "Butch" Wesley +#Copyright (c) 2019 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 @@ -37,7 +37,7 @@ # See the readme for a list of options and examples. You can also use the -gh # option to get more information about how to use the command line interface. # -# Version 6.6.1 +# Version 6.6.2 ################################################################################ extends SceneTree @@ -69,7 +69,7 @@ class CmdLineParser: else: return -1 - # Parse out the value of an option. Values are seperated from + # Parse out the value of an option. Values are separated from # the option name with "=" func get_option_value(full_option): var split = full_option.split('=') @@ -222,8 +222,8 @@ class Options: # 3. default value # # The idea is that you set the base_opts. That will get you a copies of the -# hash with null values for the other types of values. Lower precendeted hashes -# will punch through null values of higher precedednted hashes. +# hash with null values for the other types of values. Lower precedented hashes +# will punch through null values of higher precedented hashes. #------------------------------------------------------------------------------- class OptionResolver: var base_opts = null @@ -284,6 +284,7 @@ class OptionResolver: # Here starts the actual script that uses the Options class to kick off Gut # and run your tests. #------------------------------------------------------------------------------- +var _utils = load('res://addons/gut/utils.gd').new() # instance of gut var _tester = null # array of command line options specified @@ -308,7 +309,8 @@ var options = { config_file = 'res://.gutconfig.json', inner_class = '', opacity = 100, - include_subdirs = false + include_subdirs = false, + double_strategy = 'partial' } # flag to say if we should run the scripts or not. It is only @@ -347,12 +349,12 @@ func setup_options(): opts.add('-gopacity', 100, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.') opts.add('-gpo', false, 'Print option values from all sources and the value used, then quit.') opts.add('-ginclude_subdirs', false, 'Include subdirectories of -gdir.') + opts.add('-gdouble_strategy', 'partial', 'Default strategy to use when doubling. Valid values are [partial, full]. Default "[default]"') return opts # Parses options, applying them to the _tester or setting values # in the options struct. - func extract_command_line_options(from, to): to.tests = from.get_value('-gtest') to.dirs = from.get_value('-gdir') @@ -369,6 +371,7 @@ func extract_command_line_options(from, to): to.inner_class = from.get_value('-ginner_class') to.opacity = from.get_value('-gopacity') to.include_subdirs = from.get_value('-ginclude_subdirs') + to.double_strategy = from.get_value('-gdouble_strategy') func get_value(dict, index, default): if(dict.has(index)): @@ -405,6 +408,7 @@ func load_options_from_config_file(file_path, into): into.log_level = get_value(results.result, 'log', 1) into.inner_class = get_value(results.result, 'inner_class', '') into.opacity = get_value(results.result, 'opacity', 100) + into.double_strategy = get_value(results.result, 'double_strategy', 'partial') return 1 @@ -439,6 +443,11 @@ func apply_options(opts): if(!_auto_run): _tester.p("Could not find a script that matched: " + opts.selected) + if(opts.double_strategy == 'full'): + _tester.set_double_strategy(_utils.DOUBLE_STRATEGY.FULL) + elif(opts.double_strategy == 'partial'): + _tester.set_double_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) + _tester.set_unit_test_name(opts.unit_test_name) diff --git a/addons/gut/gut_gui.gd b/addons/gut/gut_gui.gd index 252c5222..124352b5 100644 --- a/addons/gut/gut_gui.gd +++ b/addons/gut/gut_gui.gd @@ -2,7 +2,7 @@ #The MIT License (MIT) #===================== # -#Copyright (c) 2017 Tom "Butch" Wesley +#Copyright (c) 2019 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 diff --git a/addons/gut/logger.gd b/addons/gut/logger.gd new file mode 100644 index 00000000..04760e56 --- /dev/null +++ b/addons/gut/logger.gd @@ -0,0 +1,18 @@ + + + + + +func warn(text): + print('WARNING: ', text) + +func error(text): + print('ERROR: ', text) + +func info(text): + print('INFO: ', text) + +func debug(text): + print('DEBUG: ', text) + +# TODO deprecated diff --git a/addons/gut/method_maker.gd b/addons/gut/method_maker.gd new file mode 100644 index 00000000..7a4e0c6f --- /dev/null +++ b/addons/gut/method_maker.gd @@ -0,0 +1,194 @@ +# This class will generate method decleration lines based on method meta +# data. It will create defaults that match the method data. +# +# -------------------- +# function meta data +# -------------------- +# name: +# flags: +# args: [{ +# (class_name:), +# (hint:0), +# (hint_string:), +# (name:), +# (type:4), +# (usage:7) +# }] +# default_args [] + +var _utils = load('res://addons/gut/utils.gd').new() +var _lgr = _utils.get_logger() +const PARAM_PREFIX = 'p_' + +# ------------------------------------------------------ +# _supported_defaults +# +# This array contains all the data types that are supported for default values. +# If a value is supported it will contain either an empty string or a prefix +# that should be used when setting the parameter default value. +# For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false) +# but things like Vectors and Colors do since only the parameters to create a +# new Vecotr or Color are included in the metadata. +# ------------------------------------------------------ + # TYPE_NIL = 0 — Variable is of type nil (only applied for null). + # TYPE_BOOL = 1 — Variable is of type bool. + # TYPE_INT = 2 — Variable is of type int. + # TYPE_REAL = 3 — Variable is of type float/real. + # TYPE_STRING = 4 — Variable is of type String. + # TYPE_VECTOR2 = 5 — Variable is of type Vector2. + # TYPE_RECT2 = 6 — Variable is of type Rect2. + # TYPE_VECTOR3 = 7 — Variable is of type Vector3. + # TYPE_COLOR = 14 — Variable is of type Color. + # TYPE_OBJECT = 17 — Variable is of type Object. + # TYPE_DICTIONARY = 18 — Variable is of type Dictionary. + # TYPE_ARRAY = 19 — Variable is of type Array. + # TYPE_VECTOR2_ARRAY = 24 — Variable is of type PoolVector2Array. + + + +# TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D. +# TYPE_PLANE = 9 — Variable is of type Plane. +# TYPE_QUAT = 10 — Variable is of type Quat. +# TYPE_AABB = 11 — Variable is of type AABB. +# TYPE_BASIS = 12 — Variable is of type Basis. +# TYPE_TRANSFORM = 13 — Variable is of type Transform. +# TYPE_NODE_PATH = 15 — Variable is of type NodePath. +# TYPE_RID = 16 — Variable is of type RID. +# TYPE_RAW_ARRAY = 20 — Variable is of type PoolByteArray. +# TYPE_INT_ARRAY = 21 — Variable is of type PoolIntArray. +# TYPE_REAL_ARRAY = 22 — Variable is of type PoolRealArray. +# TYPE_STRING_ARRAY = 23 — Variable is of type PoolStringArray. +# TYPE_VECTOR3_ARRAY = 25 — Variable is of type PoolVector3Array. +# TYPE_COLOR_ARRAY = 26 — Variable is of type PoolColorArray. +# TYPE_MAX = 27 — Marker for end of type constants. +# ------------------------------------------------------ +var _supported_defaults = [] + +func _init(): + for i in range(TYPE_MAX): + _supported_defaults.append(null) + + # These types do not require a prefix for defaults + _supported_defaults[TYPE_NIL] = '' + _supported_defaults[TYPE_BOOL] = '' + _supported_defaults[TYPE_INT] = '' + _supported_defaults[TYPE_REAL] = '' + _supported_defaults[TYPE_OBJECT] = '' + _supported_defaults[TYPE_ARRAY] = '' + _supported_defaults[TYPE_STRING] = '' + _supported_defaults[TYPE_DICTIONARY] = '' + _supported_defaults[TYPE_VECTOR2_ARRAY] = '' + + # These require a prefix for whatever default is provided + _supported_defaults[TYPE_VECTOR2] = 'Vector2' + _supported_defaults[TYPE_RECT2] = 'Rect2' + _supported_defaults[TYPE_VECTOR3] = 'Vector3' + _supported_defaults[TYPE_COLOR] = 'Color' + +# ############### +# Private +# ############### + +func _is_supported_default(type_flag): + return type_flag >= 0 and type_flag < _supported_defaults.size() and [type_flag] != null + +# Creates a list of paramters with defaults of null unless a default value is +# found in the metadata. If a default is found in the meta then it is used if +# it is one we know how support. +# +# If a default is found that we don't know how to handle then this method will +# return null. +func _get_arg_text(method_meta): + var text = '' + var args = method_meta.args + var defaults = [] + var has_unsupported_defaults = false + + # fill up the defaults with null defaults for everything that doesn't have + # a default in the meta data. default_args is an array of default values + # for the last n parameters where n is the size of default_args so we only + # add nulls for everything up to the first parameter with a default. + for i in range(args.size() - method_meta.default_args.size()): + defaults.append('null') + + # Add meta-data defaults. + for i in range(method_meta.default_args.size()): + var t = args[defaults.size()]['type'] + var value = '' + if(_is_supported_default(t)): + # strings are special, they need quotes around the value + if(t == TYPE_STRING): + value = str("'", str(method_meta.default_args[i]), "'") + # Colors need the parens but things like Vector2 and Rect2 don't + elif(t == TYPE_COLOR): + value = str(_supported_defaults[t], '(', str(method_meta.default_args[i]), ')') + # Everything else puts the prefix (if one is there) fomr _supported_defaults + # in front. The to_lower is used b/c for some reason the defaults for + # null, true, false are all "Null", "True", "False". + else: + value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower()) + else: + _lgr.warn(str( + 'Unsupported default param type: ',method_meta.name, '-', args[defaults.size()].name, ' ', t, ' = ', method_meta.default_args[i])) + value = str('unsupported=',t) + has_unsupported_defaults = true + + defaults.append(value) + + # construct the string of parameters + for i in range(args.size()): + text += str(PARAM_PREFIX, args[i].name, '=', defaults[i]) + if(i != args.size() -1): + text += ', ' + + # if we don't know how to make a default then we have to return null b/c + # it will cause a runtime error and it's one thing we could return to let + # callers know it didn't work. + if(has_unsupported_defaults): + text = null + + return text + +# ############### +# Public +# ############### + +# Creates a delceration for a function based off of function metadata. All +# types whose defaults are supported will have their values. If a datatype +# is not supported and the paramter has a default, a warning message will be +# printed and the decleration will return null. +func get_decleration_text(meta): + var param_text = _get_arg_text(meta) + var text = null + if(param_text != null): + text = str('func ', meta.name, '(', param_text, '):') + return text + +# creates a call to the function in meta in the super's class. +func get_super_call_text(meta): + var params = '' + var all_supported = true + + for i in range(meta.args.size()): + params += PARAM_PREFIX + meta.args[i].name + if(meta.args.size() > 1 and i != meta.args.size() -1): + params += ', ' + + return str('.', meta.name, '(', params, ')') + +func get_spy_call_parameters_text(meta): + var called_with = 'null' + if(meta.args.size() > 0): + called_with = '[' + for i in range(meta.args.size()): + called_with += str(PARAM_PREFIX, meta.args[i].name) + if(i < meta.args.size() - 1): + called_with += ', ' + called_with += ']' + return called_with + +func get_logger(): + return _lgr + +func set_logger(logger): + _lgr = logger diff --git a/addons/gut/plugin.cfg b/addons/gut/plugin.cfg index 6602f19f..96636b1a 100644 --- a/addons/gut/plugin.cfg +++ b/addons/gut/plugin.cfg @@ -3,5 +3,5 @@ name="Gut" description="Unit Testing tool for Godot." author="Butch Wesley" -version="6.6.1" +version="6.6.2" script="gut_plugin.gd" diff --git a/addons/gut/signal_watcher.gd b/addons/gut/signal_watcher.gd index d4e35fa9..e278089f 100644 --- a/addons/gut/signal_watcher.gd +++ b/addons/gut/signal_watcher.gd @@ -2,7 +2,7 @@ #The MIT License (MIT) #===================== # -#Copyright (c) 2017 Tom "Butch" Wesley +#Copyright (c) 2019 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 diff --git a/addons/gut/stub_params.gd b/addons/gut/stub_params.gd index d0edaaae..4211dd24 100644 --- a/addons/gut/stub_params.gd +++ b/addons/gut/stub_params.gd @@ -1,12 +1,14 @@ var return_val = null var stub_target = null +var target_subpath = null var parameters = null var stub_method = null const NOT_SET = '|_1_this_is_not_set_1_|' -func _init(target=null, method=null): +func _init(target=null, method=null, subpath=null): stub_target = target stub_method = method + target_subpath = subpath func to_return(val): return_val = val @@ -21,3 +23,6 @@ func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_S else: idx += 1 return self + +func to_s(): + return str(stub_target, '(', target_subpath, ').', stub_method, ' with (', parameters, ') = ', return_val) diff --git a/addons/gut/stubber.gd b/addons/gut/stubber.gd index 7dae50b6..6d829be4 100644 --- a/addons/gut/stubber.gd +++ b/addons/gut/stubber.gd @@ -9,27 +9,39 @@ # } # } var returns = {} -var StubParams = load('res://addons/gut/stub_params.gd') var _gut = null +var _utils = load('res://addons/gut/utils.gd').new() func _is_instance(obj): return typeof(obj) == TYPE_OBJECT and !obj.has_method('new') -func _get_path_from_variant(obj): +func _make_key_from_metadata(doubled): + var to_return = doubled.__gut_metadata_.path + if(doubled.__gut_metadata_.subpath != ''): + to_return += str('-', doubled.__gut_metadata_.subpath) + return to_return + +# Creates they key for the returns hash based on the type of object passed in +# obj could be a string of a path to a script with an optional subpath or +# it could be an instance of a doubled object. +func _make_key_from_variant(obj, subpath=null): var to_return = null match typeof(obj): TYPE_STRING: + # this has to match what is done in _make_key_from_metadata to_return = obj + if(subpath != null): + to_return += str('-', subpath) TYPE_OBJECT: if(_is_instance(obj)): - to_return = obj.get_script().get_path() + to_return = _make_key_from_metadata(obj) else: to_return = obj.resource_path return to_return -func _add_obj_method(obj, method): - var key = _get_path_from_variant(obj) +func _add_obj_method(obj, method, subpath=null): + var key = _make_key_from_variant(obj, subpath) if(_is_instance(obj)): key = obj @@ -43,26 +55,44 @@ func _add_obj_method(obj, method): # ############## # Public # ############## -func set_return(obj, method, value, parameters = null): + +# TODO: This method is only used in tests and should be refactored out. It +# does not support inner classes and isn't helpful. +func set_return(obj, method, value, parameters=null): var key = _add_obj_method(obj, method) - var sp = StubParams.new(key, method) + var sp = _utils.StubParams.new(key, method) sp.parameters = parameters sp.return_val = value returns[key][method].append(sp) func add_stub(stub_params): - var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method) + var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method, stub_params.target_subpath) returns[key][stub_params.stub_method].append(stub_params) -func get_return(obj, method, parameters = null): - var key = _get_path_from_variant(obj) +# Gets a stubbed return value for the object and method passed in. If the +# instance was stubbed it will use that, otherwise it will use the path and +# subpath of the object to try to find a value. +# +# It will also use the optional list of parameter values to find a value. If +# the objet was stubbed with no parameters than any parameters will match. +# If it was stubbed with specific paramter values then it will try to match. +# If the parameters do not match BUT there was also an empty paramter list stub +# then it will return those. +# If it cannot find anything that matches then null is returned.for +# +# Parameters +# obj: this should be an instance of a doubled object. +# method: the method called +# paramters: optional array of paramter vales to find a return value for. +func get_return(obj, method, parameters=null): + var key = _make_key_from_variant(obj) var to_return = null if(_is_instance(obj)): if(returns.has(obj) and returns[obj].has(method)): key = obj elif(obj.get('__gut_metadata_')): - key = obj.__gut_metadata_.path + key = _make_key_from_metadata(obj) if(returns.has(key) and returns[key].has(method)): var param_idx = -1 @@ -89,3 +119,13 @@ func set_gut(gut): func clear(): returns.clear() + +func to_s(): + var text = '' + for thing in returns: + text += str(thing) + "\n" + for method in returns[thing]: + text += str("\t", method, "\n") + for i in range(returns[thing][method].size()): + text += "\t\t" + returns[thing][method][i].to_s() + "\n" + return text diff --git a/addons/gut/test.gd b/addons/gut/test.gd index a4a9787d..f98c5909 100644 --- a/addons/gut/test.gd +++ b/addons/gut/test.gd @@ -5,7 +5,7 @@ #The MIT License (MIT) #===================== # -#Copyright (c) 2017 Tom "Butch" Wesley +#Copyright (c) 2019 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 @@ -37,9 +37,8 @@ ################################################################################ extends Node -# constant for signal when calling yeild_for +# constant for signal when calling yield_for const YIELD = 'timeout' -var StubParams = load('res://addons/gut/stub_params.gd') # Need a reference to the instance that is running the tests. This # is set by the gut class when it runs the tests. This gets you @@ -104,8 +103,13 @@ var _summary = { # This is used to watch signals so we can make assertions about them. var _signal_watcher = load('res://addons/gut/signal_watcher.gd').new() +# Convenience copy of _utils.DOUBLE_STRATEGY +var DOUBLE_STRATEGY = null + +var _utils = load('res://addons/gut/utils.gd').new() func _init(): _init_types_dictionary() + DOUBLE_STRATEGY = _utils.DOUBLE_STRATEGY # ------------------------------------------------------------------------------ # Fail an assertion. Causes test and script to fail as well. @@ -149,7 +153,7 @@ func _do_datatypes_match__fail_if_not(got, expected, text): # print out a warning but do not fail. if([2, 3].has(got_type) and [2, 3].has(expect_type)): if(gut): - gut.p(str('Warn: Float/Int comparison. Got ', types[got_type], ' but expected ', types[expect_type]), 1) + gut.get_logger().warn(str('Warn: Float/Int comparison. Got ', types[got_type], ' but expected ', types[expect_type])) else: _fail('Cannot compare ' + types[got_type] + '[' + str(got) + '] to ' + types[expect_type] + '[' + str(expected) + ']. ' + text) passed = false @@ -666,6 +670,9 @@ func assert_string_ends_with(text, search, match_case=true): # ------------------------------------------------------------------------------ # Assert that a method was called on an instance of a doubled class. If # parameters are supplied then the params passed in when called must match. +# TODO make 3rd paramter "param_or_text" and add fourth parameter of "text" and +# then work some magic so this can have a "text" parameter without being +# annoying. # ------------------------------------------------------------------------------ func assert_called(inst, method_name, parameters=null): var disp = str('Expected [',method_name,'] to have been called on ',inst) @@ -686,7 +693,7 @@ func assert_called(inst, method_name, parameters=null): # sent matching parameters. # ------------------------------------------------------------------------------ func assert_not_called(inst, method_name, parameters=null): - var disp = str('Expected [', method_name, '] to NOT hvae been called on ', inst) + var disp = str('Expected [', method_name, '] to NOT have been called on ', inst) if(!inst.get('__gut_metadata_')): _fail('You must pass a doubled instance to assert_not_called. Check the wiki for info on using double.') @@ -720,6 +727,26 @@ func assert_call_count(inst, method_name, expected_count, parameters=null): else: _fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst))) +# ------------------------------------------------------------------------------ +# Asserts the passed in value is null +# ------------------------------------------------------------------------------ +func assert_null(got, text=''): + var disp = str('Expected [', got, '] to be NULL: ', text) + if(got == null): + _pass(disp) + else: + _fail(disp) + +# ------------------------------------------------------------------------------ +# Asserts the passed in value is null +# ------------------------------------------------------------------------------ +func assert_not_null(got, text=''): + var disp = str('Expected [', got, '] to be anything but NULL: ', text) + if(got == null): + _fail(disp) + else: + _pass(disp) + # ------------------------------------------------------------------------------ # Mark the current test as pending. # ------------------------------------------------------------------------------ @@ -738,16 +765,28 @@ func pending(text=""): # is not being watched. # ------------------------------------------------------------------------------ -# I think this reads better than set_yield_time, but don't want to break anything +# ------------------------------------------------------------------------------ +# Yield for the time sent in. The optional message will be printed when +# Gut detects the yeild. When the time expires the YIELD signal will be +# emitted. +# ------------------------------------------------------------------------------ func yield_for(time, msg=''): return gut.set_yield_time(time, msg) +# ------------------------------------------------------------------------------ +# Yield to a signal or a maximum amount of time, whichever comes first. When +# the conditions are met the YIELD signal will be emitted. +# ------------------------------------------------------------------------------ func yield_to(obj, signal_name, max_wait, msg=''): watch_signals(obj) gut.set_yield_signal_or_time(obj, signal_name, max_wait, msg) return gut +# ------------------------------------------------------------------------------ +# Ends a test that had a yield in it. You only need to use this if you do +# not make assertions after a yield. +# ------------------------------------------------------------------------------ func end_test(): gut.end_yielded_test() @@ -769,6 +808,14 @@ func get_assert_count(): func clear_signal_watcher(): _signal_watcher.clear() +func get_double_strategy(): + return gut.get_doubler().get_strategy() + +func set_double_strategy(double_strategy): + gut.get_doubler().set_strategy(double_strategy) + +func pause_before_teardown(): + gut.pause_before_teardown() # ------------------------------------------------------------------------------ # Convert the _summary dictionary into text # ------------------------------------------------------------------------------ @@ -781,13 +828,82 @@ func get_summary_text(): to_return += str("\n ", _summary.failed, ' failed.') return to_return -func double(thing): - return gut.get_doubler().double(thing) +# ------------------------------------------------------------------------------ +# Double a script, inner class, or scene using a path or a loaded script/scene. +# +# +# ------------------------------------------------------------------------------ +func double(thing, p2=null, p3=null): + var strategy = p2 + var subpath = null + + if(typeof(p2) == TYPE_STRING): + strategy = p3 + subpath = p2 + + var path = null + if(typeof(thing) == TYPE_OBJECT): + path = thing.resource_path + else: + path = thing + + var extension = path.get_extension() + var to_return = null + + if(extension == 'tscn'): + to_return = double_scene(path, strategy) + elif(extension == 'gd'): + if(subpath == null): + to_return = double_script(path, strategy) + else: + to_return = double_inner(path, subpath, strategy) + + return to_return + +# ------------------------------------------------------------------------------ +# Specifically double a scene +# ------------------------------------------------------------------------------ +func double_scene(path, strategy=null): + var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy()) + return gut.get_doubler().double_scene(path, override_strat) + +# ------------------------------------------------------------------------------ +# Specifically double a script +# ------------------------------------------------------------------------------ +func double_script(path, strategy=null): + var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy()) + return gut.get_doubler().double(path, override_strat) -func double_scene(thing): - return gut.get_doubler().double_scene(thing) +# ------------------------------------------------------------------------------ +# Specifically double an Inner class in a a script +# ------------------------------------------------------------------------------ +func double_inner(path, subpath, strategy=null): + var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy()) + return gut.get_doubler().double_inner(path, subpath, override_strat) -func stub(thing, method_name): - var sp = StubParams.new(thing, method_name) +# ------------------------------------------------------------------------------ +# Stub something. +# +# Parameters +# 1: the thing to stub, a file path or a instance or a class +# 2: either an inner class subpath or the method name +# 3: the method name if an inner class subpath was specified +# NOTE: right now we cannot stub inner classes at the path level so this should +# only be called with two parameters. I did the work though so I'm going +# to leave it but not update the wiki. +# ------------------------------------------------------------------------------ +func stub(thing, p2, p3=null): + var method_name = p2 + var subpath = null + if(p3 != null): + subpath = p2 + method_name = p3 + var sp = _utils.StubParams.new(thing, method_name, subpath) gut.get_stubber().add_stub(sp) return sp + +# ------------------------------------------------------------------------------ +# convenience wrapper. +# ------------------------------------------------------------------------------ +func simulate(obj, times, delta): + gut.simulate(obj, times, delta) diff --git a/addons/gut/utils.gd b/addons/gut/utils.gd new file mode 100644 index 00000000..8ce82a53 --- /dev/null +++ b/addons/gut/utils.gd @@ -0,0 +1,55 @@ +var _Logger = load('res://addons/gut/logger.gd') # everything should use get_logger +var Stubber = load('res://addons/gut/stubber.gd') +var Doubler = load('res://addons/gut/doubler.gd') +var Spy = load('res://addons/gut/spy.gd') +var StubParams = load('res://addons/gut/stub_params.gd') + +enum DOUBLE_STRATEGY{ + FULL, + PARTIAL +} + +# ------------------------------------------------------------------------------ +# Everything should get a logger through this. +# +# Eventually I want to make this get a single instance of a logger but I'm not +# sure how to do that without everything having to be in the tree which I +# DO NOT want to to do. I'm thinking of writings some instance ids to a file +# and loading them in the _init for this. +# ------------------------------------------------------------------------------ +func get_logger(): + return _Logger.new() + +# ------------------------------------------------------------------------------ +# Returns an array created by splitting the string by the delimiter +# ------------------------------------------------------------------------------ +func split_string(to_split, delim): + var to_return = [] + + var loc = to_split.find(delim) + while(loc != -1): + to_return.append(to_split.substr(0, loc)) + to_split = to_split.substr(loc + 1, to_split.length() - loc) + loc = to_split.find(delim) + to_return.append(to_split) + return to_return + +# ------------------------------------------------------------------------------ +# Returns a string containing all the elements in the array seperated by delim +# ------------------------------------------------------------------------------ +func join_array(a, delim): + var to_return = '' + for i in range(a.size()): + to_return += str(a[i]) + if(i != a.size() -1): + to_return += str(delim) + return to_return + +# ------------------------------------------------------------------------------ +# return if_null if value is null otherwise return value +# ------------------------------------------------------------------------------ +func nvl(value, if_null): + if(value == null): + return if_null + else: + return value diff --git a/scenes/main.tscn b/scenes/main.tscn index 5da9ec36..57195bd1 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -49,6 +49,7 @@ _file_extension = ".gd" _inner_class_prefix = "Test" _temp_directory = "user://gut_temp_directory" _include_subdirectories = false +_double_strategy = 0 _directory1 = "res://test/unit" _directory2 = "res://test/integration" _directory3 = "" diff --git a/scratch/find_bad_methods.gd b/scratch/find_bad_methods.gd new file mode 100644 index 00000000..8dc06c09 --- /dev/null +++ b/scratch/find_bad_methods.gd @@ -0,0 +1,108 @@ +extends SceneTree +# ############################################################################## +# I used to this to try and find all the built-in methods that I shouldn't +# double. it has some useful examples in it so I'll keep it around for a bit. +# ############################################################################## + +var Doubler = load('res://addons/gut/doubler.gd') + +var DOUBLE_ME_PATH = 'res://test/doubler_test_objects/double_me.gd' +var DoubleMe = load(DOUBLE_ME_PATH) + +var DOUBLE_EXTENDS_NODE2D = 'res://test/doubler_test_objects/double_extends_node2d.gd' +var DoubleExtendsNode2d = load(DOUBLE_EXTENDS_NODE2D) + +var ARGS = 'args' +var FLAGS = 'flags' +var NAME = 'name' + +var _local_black_list = ['draw_char'] + +func it_worked(path): + print('it worked: ', path) + +func it_didnt(path): + print("it didn't work: ", path) + +# gets a list of all the unique methods that aren't of flag==65. +func get_full_blacklist(obj): + var list = [] + var methods = obj.get_method_list() + + for i in range(methods.size()): + var flag = methods[i][FLAGS] + var name = methods[i][NAME] + + if(flag != 65): + if(!list.has(name)): + list.append(name) + + return list + +# make a new doubler with common props. +func new_doubler(): + var doubler = Doubler.new() + doubler.set_output_dir('user://doubler_temp_files/') + return doubler + +# Use the output from this method to determine which methods should be added +# to the doubler's blacklist. Each iteration of the loop a method is removed +# from the blacklist and a double is made. Check for errors in the output to +# find methods that should not be doubled. +# +# Creates a blacklist and puts it on the doubler. It then loops through all of +# them removing the first element from the array and trying to double the object. +# It then adds the removed one at the end so that we don't keep getting errors +# for each bad method. +# +# This also uses a local blacklist with methods that cause everytning to blow +# up. These methods should also be in doubler's blacklist. +func remove_methods_from_blacklist_one_by_one(obj, path): + var doubler = new_doubler() + doubler._blacklist = get_full_blacklist(obj) + + for i in range(doubler._blacklist.size()): + + var removed = doubler._blacklist[0] + doubler._blacklist.remove(0) + if(!_local_black_list.has(removed)): + print(removed) + var inst = doubler.double(path) + if(inst == null): + print("didn't work") + else: + print('skipped ', removed) + + doubler._blacklist.append(removed) + + doubler.clear_output_directory() + +# given a path it will create a double of it and then create an instance of the +# doubled object checking for nulls along the way. Thi is what I used to test +# the black lists for various objects. +func double_and_instance_it(path): + var doubler = new_doubler() + + var doubled = doubler.double(path) + var inst = null + if(doubled == null): + it_didnt(path) + else: + inst = doubled.new() + if(inst != null): + it_worked(path) + else: + it_didnt(path) + doubler.clear_output_directory() + return inst + +# main +func _init(): + var n = Node2D.new() + remove_methods_from_blacklist_one_by_one(DoubleMe.new(), DOUBLE_ME_PATH) + var inst = double_and_instance_it(DOUBLE_ME_PATH) + + remove_methods_from_blacklist_one_by_one(DoubleExtendsNode2d.new(), DOUBLE_EXTENDS_NODE2D) + inst = double_and_instance_it(DOUBLE_EXTENDS_NODE2D) + n.print_stray_nodes() + quit() diff --git a/scratch/get_info.gd b/scratch/get_info.gd new file mode 100644 index 00000000..4b430fb3 --- /dev/null +++ b/scratch/get_info.gd @@ -0,0 +1,127 @@ +# ############################################################################## +# This is a script I used to poke around script and method properties. It's +# not supposed to be a "real" tool but it has a lot of examples in it and has +# come in handy numerous times. +# +# Feel free to use whatever you find in here for your own purposes. +# ############################################################################## +extends SceneTree + +const DEFAULT_ARGS = 'default_args' +const NAME = 'name' +const ARGS = 'args' + + +class ExtendsNode2D: + extends Node2D + + func my_function(): + return 7 + + func get_position(): + return Vector2(0, 0) + +class SimpleObject: + var a = 'a' + +func get_methods_by_flag(obj): + var methods_by_flags = {} + var methods = obj.get_method_list() + + for i in range(methods.size()): + var flag = methods[i]['flags'] + var name = methods[i]['name'] + if(methods_by_flags.has(flag)): + methods_by_flags[flag].append(name) + else: + methods_by_flags[flag] = [name] + return methods_by_flags + +func print_methods_by_flags(methods_by_flags): + + for flag in methods_by_flags: + for i in range(methods_by_flags[flag].size()): + print(flag, ": ", methods_by_flags[flag][i]) + + var total = 0 + for flag in methods_by_flags: + if(methods_by_flags[flag].size() > 0): + print("-- ", flag, " (", methods_by_flags[flag].size(), ") --") + total += methods_by_flags[flag].size() + print("Total: ", total) + +func subtract_dictionary(sub_this, from_this): + var result = {} + for key in sub_this: + if(from_this.has(key)): + result[key] = [] + + for value in from_this[key]: + var index = sub_this[key].find(value) + if(index == -1): + result[key].append(value) + return result + +func print_method_info(obj): + var methods = obj.get_method_list() + + for i in range(methods.size()): + print(methods[i]['name']) + if(methods[i]['default_args'].size() > 0): + print(" *** here be defaults ***") + + for key in methods[i]: + if(key == 'args'): + print(' args:') + for argname in range(methods[i][key].size()): + print(' ', methods[i][key][argname]['name'], ': ', methods[i][key][argname]) + else: + print(' ', key, ': ', methods[i][key]) + +func print_a_bunch_of_methods_by_flags(): + var e = ExtendsNode2D.new() + var n = get_methods_by_flag(e) + # print_methods_by_flags(n) + + var s = SimpleObject.new() + var o = get_methods_by_flag(s) + # print_methods_by_flags(o) + + #print_methods_by_flags(subtract_dictionary(n, o)) + print("\n\n\n") + print_methods_by_flags(subtract_dictionary(o, n)) + print("strays ") + e.print_stray_nodes() + +func get_defaults_and_types(method_meta): + var text = "" + text += method_meta[NAME] + "\n" + for i in range(method_meta[DEFAULT_ARGS].size()): + var arg_index = method_meta[ARGS].size() - (method_meta[DEFAULT_ARGS].size() - i) + text += str(' ', method_meta[ARGS][arg_index][NAME]) + text += str('(type=', method_meta[ARGS][arg_index]['type'], ")") + text += str(' = ', method_meta[DEFAULT_ARGS][i], "\n") + # text += str(' ', method_meta[ARGS][arg_index]['usage'], "\n") + return text + + +func _init(): + # var double_me = load('res://test/doubler_test_objects/double_me.gd').new() + # print_method_info(double_me) + # print("-------------\n-\n-\n-\n-------------") + # var methods = get_methods_by_flag(double_me) + # print_methods_by_flags(methods) + + #print_a_bunch_of_methods_by_flags() + #var obj = ExtendsNode2D.new() + #var obj = load('res://addons/gut/gut.gd').new() + var obj = load('res://test/doubler_test_objects/double_extends_window_dialog.gd').new() + #print_method_info(obj) + print_method_info(obj) + var methods = obj.get_method_list() + + for i in range(methods.size()): + if(methods[i][DEFAULT_ARGS].size() > 0): + pass#print(get_defaults_and_types(methods[i])) + + quit() diff --git a/scratch/make_class_from_string.gd b/scratch/make_class_from_string.gd new file mode 100644 index 00000000..8f137438 --- /dev/null +++ b/scratch/make_class_from_string.gd @@ -0,0 +1,53 @@ +extends SceneTree +# ############################################################################## +# Proof of concept to create a class from a string instead of writing it to a +# file and then loading it. +# +# I hope to use this to create doubles in the future, though it will make +# debugging the generated code a little harder, it always bothered me that I +# was writing and reading files. It's slower and wastes cycles on the HD. +# Probably shouldn't bother me that much, but it does.. +# ############################################################################## + +func make_class(): + var text = "" + + text = "class MadeIt:\n" + \ + "\tvar something=\"hello\"\n" + \ + "\tfunc do_something():\n" +\ + "\t\treturn 'did it'" + + return text + +func make_node(): + var text = "extends Node2D\n" + \ + "func do_something():\n" + \ + "\treturn 'did it'" + return text + + +func get_script_for_text(text): + var script = GDScript.new() + script.set_source_code(text) + script.reload() + return script + +func create_node2d(): + var n = Node2D.new() + n.set_script(get_script_for_text(make_node())) + print(n.do_something()) + n.free() + +func create_instance(): + var obj = Reference.new() + obj.set_script(get_script_for_text(make_class())) + + var inner_class = obj.MadeIt.new() + print(inner_class.do_something()) + + + +func _init(): + print("hello world") + create_node2d() + quit() diff --git a/test/doubler_test_objects/double_extends_window_dialog.gd b/test/doubler_test_objects/double_extends_window_dialog.gd new file mode 100644 index 00000000..dfa69016 --- /dev/null +++ b/test/doubler_test_objects/double_extends_window_dialog.gd @@ -0,0 +1 @@ +extends WindowDialog diff --git a/test/doubler_test_objects/double_me.gd b/test/doubler_test_objects/double_me.gd index 11968b38..92cc3a2b 100644 --- a/test/doubler_test_objects/double_me.gd +++ b/test/doubler_test_objects/double_me.gd @@ -1,16 +1,19 @@ var _value = 0 func get_value(): - return _value + return _value func set_value(val): - _value = val + _value = val func has_one_param(one): - pass + pass func has_two_params_one_default(one, two=null): - pass + pass func get_position(): - return .get_position() + return .get_position() + +func has_string_and_array_defaults(string_param = "asdf", array_param = [1]): + pass diff --git a/test/doubler_test_objects/inner_classes.gd b/test/doubler_test_objects/inner_classes.gd new file mode 100644 index 00000000..2dde7c5e --- /dev/null +++ b/test/doubler_test_objects/inner_classes.gd @@ -0,0 +1,30 @@ +var _value = 1 + +class InnerA: + func get_a(): + return 'a' +# Needed another class with same method as an inner class to test +# stubbing. +class AnotherInnerA: + func get_a(): + return 'aia' + +class InnerB: + func get_b(): + return 'b' + + class InnerB1: + func get_b1(): + return 'b1' + +class InnerCA: + extends InnerA + + func get_ca(): + return 'ca' + +func get_value(): + return _value + +func set_value(val): + _value = val diff --git a/test/gut_test.gd b/test/gut_test.gd new file mode 100644 index 00000000..4fe941cd --- /dev/null +++ b/test/gut_test.gd @@ -0,0 +1,19 @@ +extends 'res://addons/gut/test.gd' + +const DOUBLE_ME_PATH = 'res://test/doubler_test_objects/double_me.gd' +var DoubleMe = load(DOUBLE_ME_PATH) + +const DOUBLE_ME_SCENE_PATH = 'res://test/doubler_test_objects/double_me_scene.tscn' +var DoubleMeScene = load(DOUBLE_ME_SCENE_PATH) + +const DOUBLE_EXTENDS_NODE2D = 'res://test/doubler_test_objects/double_extends_node2d.gd' +var DoubleExtendsNode2D = load(DOUBLE_EXTENDS_NODE2D) + +const DOUBLE_EXTENDS_WINDOW_DIALOG = 'res://test/doubler_test_objects/double_extends_window_dialog.gd' +var DoubleExtendsWindowDialog = load(DOUBLE_EXTENDS_WINDOW_DIALOG) + +const INNER_CLASSES_PATH = 'res://test/doubler_test_objects/inner_classes.gd' +var InnerClasses = load(INNER_CLASSES_PATH) + +var Gut = load('res://addons/gut/gut.gd') +var Test = load('res://addons/gut/test.gd') diff --git a/test/integration/test_doubler_and_spy.gd b/test/integration/test_doubler_and_spy.gd index ea589524..17bcb5cc 100644 --- a/test/integration/test_doubler_and_spy.gd +++ b/test/integration/test_doubler_and_spy.gd @@ -44,3 +44,12 @@ class TestBoth: inst.has_two_params_one_default('a', 'b') assert_false(_spy.was_called(inst, 'has_two_params_one_default', ['c', 'd']), 'should not match') assert_true(_spy.was_called(inst, 'has_two_params_one_default', ['a', 'b']), 'should match') + + func test_can_spy_on_built_ins_when_doing_a_full_double(): + _doubler.set_strategy(DOUBLE_STRATEGY.FULL) + var inst = _doubler.double(DOUBLE_ME_PATH).new() + # add_user_signal is a function on Object that isn't in our subclass. + inst.add_user_signal('new_signal') + inst.add_user_signal('signal_with_params', ['a', 'b']) + assert_true(_spy.was_called(inst, 'add_user_signal'), 'added first signal') + assert_true(_spy.was_called(inst, 'add_user_signal', ['signal_with_params', ['a', 'b']]), 'second signal added') diff --git a/test/integration/test_doubler_and_stubber.gd b/test/integration/test_doubler_and_stubber.gd index fe1c1364..03924a11 100644 --- a/test/integration/test_doubler_and_stubber.gd +++ b/test/integration/test_doubler_and_stubber.gd @@ -10,60 +10,73 @@ var Doubler = load('res://addons/gut/doubler.gd') const DOUBLE_ME_PATH = 'res://test/doubler_test_objects/double_me.gd' const DOUBLE_ME_SCENE_PATH = 'res://test/doubler_test_objects/double_me_scene.tscn' +const DOUBLE_EXTENDS_NODE2D = 'res://test/doubler_test_objects/double_extends_node2d.gd' const TEMP_FILES = 'user://test_doubler_temp_file' var gr = { - doubler = null, - stubber = null + doubler = null, + stubber = null } func before_each(): - gr.doubler = Doubler.new() - gr.doubler.set_output_dir(TEMP_FILES) - gr.stubber = Stubber.new() - gr.doubler.clear_output_directory() + gr.doubler = Doubler.new() + gr.doubler.set_output_dir(TEMP_FILES) + gr.stubber = Stubber.new() + gr.doubler.clear_output_directory() func test_doubled_has_null_stubber_by_default(): - var d = gr.doubler.double(DOUBLE_ME_PATH).new() - assert_eq(d.__gut_metadata_.stubber, null) + var d = gr.doubler.double(DOUBLE_ME_PATH).new() + assert_eq(d.__gut_metadata_.stubber, null) func test_doubled_have_ref_to_stubber(): - gr.doubler.set_stubber(gr.stubber) - var d = gr.doubler.double(DOUBLE_ME_PATH).new() - assert_eq(d.__gut_metadata_.stubber, gr.stubber) + gr.doubler.set_stubber(gr.stubber) + var d = gr.doubler.double(DOUBLE_ME_PATH).new() + assert_eq(d.__gut_metadata_.stubber, gr.stubber) func test_stubbing_method_returns_expected_value(): - gr.doubler.set_stubber(gr.stubber) - var D = gr.doubler.double(DOUBLE_ME_PATH) - gr.stubber.set_return(DOUBLE_ME_PATH, 'get_value', 7) - assert_eq(D.new().get_value(), 7) + gr.doubler.set_stubber(gr.stubber) + var D = gr.doubler.double(DOUBLE_ME_PATH) + gr.stubber.set_return(DOUBLE_ME_PATH, 'get_value', 7) + assert_eq(D.new().get_value(), 7) + +func test_can_stub_non_local_methods(): + gr.doubler.set_stubber(gr.stubber) + var D = gr.doubler.double(DOUBLE_ME_PATH) + gr.stubber.set_return(DOUBLE_ME_PATH, 'get_position', Vector2(11, 11)) + assert_eq(D.new().get_position(), Vector2(11, 11)) + +func test_when_non_local_methods_not_stubbed_super_is_returned(): + gr.doubler.set_stubber(gr.stubber) + var D = gr.doubler.double(DOUBLE_EXTENDS_NODE2D) + var d = D.new() + assert_eq(d.get_rotation(), 0.0) func test_can_stub_doubled_instance_values(): - gr.doubler.set_stubber(gr.stubber) - var D = gr.doubler.double(DOUBLE_ME_PATH) - var d1 = D.new() - var d2 = D.new() - gr.stubber.set_return(DOUBLE_ME_PATH, 'get_value', 5) - gr.stubber.set_return(d1, 'get_value', 10) - assert_eq(d1.get_value(), 10, 'instance gets right value') - assert_eq(d2.get_value(), 5, 'other instance gets class value') + gr.doubler.set_stubber(gr.stubber) + var D = gr.doubler.double(DOUBLE_ME_PATH) + var d1 = D.new() + var d2 = D.new() + gr.stubber.set_return(DOUBLE_ME_PATH, 'get_value', 5) + gr.stubber.set_return(d1, 'get_value', 10) + assert_eq(d1.get_value(), 10, 'instance gets right value') + assert_eq(d2.get_value(), 5, 'other instance gets class value') func test_stubbed_methods_send_parameters_in_callback(): - gr.doubler.set_stubber(gr.stubber) - gr.stubber.set_return(DOUBLE_ME_PATH, 'has_one_param', 10, [1]) - var d = gr.doubler.double(DOUBLE_ME_PATH).new() - assert_eq(d.has_one_param(1), 10) - assert_eq(d.has_one_param('asdf'), null) + gr.doubler.set_stubber(gr.stubber) + gr.stubber.set_return(DOUBLE_ME_PATH, 'has_one_param', 10, [1]) + var d = gr.doubler.double(DOUBLE_ME_PATH).new() + assert_eq(d.has_one_param(1), 10) + assert_eq(d.has_one_param('asdf'), null) func test_stub_with_nothing_works_with_parameters(): - gr.doubler.set_stubber(gr.stubber) - gr.stubber.set_return(DOUBLE_ME_PATH, 'has_one_param', 5) - gr.stubber.set_return(DOUBLE_ME_PATH, 'has_one_param', 10, [1]) - var d = gr.doubler.double(DOUBLE_ME_PATH).new() - assert_eq(d.has_one_param(), 5) + gr.doubler.set_stubber(gr.stubber) + gr.stubber.set_return(DOUBLE_ME_PATH, 'has_one_param', 5) + gr.stubber.set_return(DOUBLE_ME_PATH, 'has_one_param', 10, [1]) + var d = gr.doubler.double(DOUBLE_ME_PATH).new() + assert_eq(d.has_one_param(), 5) func test_can_stub_doubled_scenes(): - gr.doubler.set_stubber(gr.stubber) - gr.stubber.set_return(DOUBLE_ME_SCENE_PATH, 'return_hello', 'world') - var inst = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() - assert_eq(inst.return_hello(), 'world') + gr.doubler.set_stubber(gr.stubber) + gr.stubber.set_return(DOUBLE_ME_SCENE_PATH, 'return_hello', 'world') + var inst = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() + assert_eq(inst.return_hello(), 'world') diff --git a/test/integration/test_test_stubber_doubler.gd b/test/integration/test_test_stubber_doubler.gd index 102362f5..a58423fb 100644 --- a/test/integration/test_test_stubber_doubler.gd +++ b/test/integration/test_test_stubber_doubler.gd @@ -1,35 +1,135 @@ -extends "res://addons/gut/test.gd" +extends "res://test/gut_test.gd" -var Gut = load('res://addons/gut/gut.gd') -var Test = load('res://addons/gut/test.gd') +class TestBasics: + extends "res://test/gut_test.gd" + const TEMP_FILES = 'user://test_doubler_temp_file' + var gr = { + gut = null, + test = null + } -const TEMP_FILES = 'user://test_doubler_temp_file' + var _last_double_count = 0 -const DOUBLE_ME_PATH = 'res://test/doubler_test_objects/double_me.gd' + func before_each(): + gr.gut = Gut.new() + gr.test = Test.new() + gr.test.gut = gr.gut + # forces everything to have a unique name across tests + gr.gut.get_doubler()._double_count = _last_double_count -var gr = { - gut = null, - test = null -} + func after_each(): + _last_double_count = gr.gut.get_doubler()._double_count + gr.gut.get_doubler().clear_output_directory() + gr.gut.get_spy().clear() -func before_each(): - gr.gut = Gut.new() - gr.test = Test.new() - gr.test.gut = gr.gut + func test_double_returns_a_class(): + var D = gr.test.double(DOUBLE_ME_PATH) + assert_ne(D.new(), null) -func after_each(): - gr.gut.get_doubler().clear_output_directory() - gr.gut.get_spy().clear() + func test_double_sets_stubber_for_doubled_class(): + var d = gr.test.double(DOUBLE_ME_PATH).new() + assert_eq(d.__gut_metadata_.stubber, gr.gut.get_stubber()) -func test_double_returns_a_class(): - var D = gr.test.double(DOUBLE_ME_PATH) - assert_ne(D.new(), null) + func test_basic_double_and_stub(): + var d = gr.test.double(DOUBLE_ME_PATH).new() + gr.test.stub(DOUBLE_ME_PATH, 'get_value').to_return(10) + assert_eq(d.get_value(), 10) -func test_double_sets_stubber_for_doubled_class(): - var d = gr.test.double(DOUBLE_ME_PATH).new() - assert_eq(d.__gut_metadata_.stubber, gr.gut.get_stubber()) + func test_get_set_double_strat(): + assert_accessors(gr.test, 'double_strategy', DOUBLE_STRATEGY.PARTIAL, DOUBLE_STRATEGY.FULL) -func test_basic_double_and_stub(): - var d = gr.test.double(DOUBLE_ME_PATH).new() - gr.test.stub(DOUBLE_ME_PATH, 'get_value').to_return(10) - assert_eq(d.get_value(), 10) + func test_when_strategy_is_full_then_supers_are_spied(): + var doubled = gr.test.double(DOUBLE_ME_PATH, DOUBLE_STRATEGY.FULL).new() + doubled.is_blocking_signals() + gr.test.assert_called(doubled, 'is_blocking_signals') + assert_eq(gr.test.get_pass_count(), 1) + + func test_when_strategy_is_partial_then_supers_are_NOT_spied_in_scripts(): + var doubled = gr.test.double(DOUBLE_ME_PATH, DOUBLE_STRATEGY.PARTIAL).new() + doubled.is_blocking_signals() + gr.test.assert_not_called(doubled, 'is_blocking_signals') + assert_eq(gr.test.get_pass_count(), 1) + + func test_can_override_strategy_when_doubling_scene(): + var doubled = gr.test.double_scene(DOUBLE_ME_SCENE_PATH, DOUBLE_STRATEGY.FULL).instance() + doubled.is_blocking_signals() + gr.test.assert_called(doubled, 'is_blocking_signals') + assert_eq(gr.test.get_pass_count(), 1) + + func test_when_strategy_is_partial_then_supers_are_NOT_spied_in_scenes(): + var doubled = gr.test.double_scene(DOUBLE_ME_SCENE_PATH, DOUBLE_STRATEGY.PARTIAL).instance() + doubled.is_blocking_signals() + gr.test.assert_not_called(doubled, 'is_blocking_signals') + assert_eq(gr.test.get_pass_count(), 1) + + func test_can_stub_inner_class_methods(): + var d = gr.gut.get_doubler().double_inner(INNER_CLASSES_PATH, 'InnerA').new() + gr.test.stub(INNER_CLASSES_PATH, 'InnerA', 'get_a').to_return(10) + assert_eq(d.get_a(), 10) + + func test_can_stub_multiple_inner_classes(): + var a = gr.gut.get_doubler().double_inner(INNER_CLASSES_PATH, 'InnerA').new() + var anotherA = gr.gut.get_doubler().double_inner(INNER_CLASSES_PATH, 'AnotherInnerA').new() + gr.test.stub(a, 'get_a').to_return(10) + gr.test.stub(anotherA, 'get_a').to_return(20) + assert_eq(a.get_a(), 10) + assert_eq(anotherA.get_a(), 20) + + func test_can_stub_multiple_inners_using_class_path_and_inner_names(): + var a = gr.gut.get_doubler().double_inner(INNER_CLASSES_PATH, 'InnerA').new() + var anotherA = gr.gut.get_doubler().double_inner(INNER_CLASSES_PATH, 'AnotherInnerA').new() + gr.test.stub(INNER_CLASSES_PATH, 'InnerA', 'get_a').to_return(10) + assert_eq(a.get_a(), 10) + assert_eq(anotherA.get_a(), null) + +class TestTestsSmartDoubleMethod: + extends "res://test/gut_test.gd" + + var _gut = null + var _test = null + + func before_all(): + _gut = Gut.new() + _test = Test.new() + _test.gut = gut + + func test_when_passed_a_script_it_doubles_script(): + var inst = _test.double(DOUBLE_ME_PATH).new() + assert_eq(inst.__gut_metadata_.path, DOUBLE_ME_PATH) + + func test_when_passed_a_scene_it_doubles_a_scene(): + var inst = _test.double(DOUBLE_ME_SCENE_PATH).instance() + assert_eq(inst.__gut_metadata_.path, DOUBLE_ME_SCENE_PATH) + + func test_when_passed_script_and_inner_it_doulbes_it(): + var inst = _test.double(INNER_CLASSES_PATH, 'InnerA').new() + assert_eq(inst.__gut_metadata_.path, INNER_CLASSES_PATH, 'check path') + assert_eq(inst.__gut_metadata_.subpath, 'InnerA', 'check subpath') + + func test_strategy_used_for_scripts(): + var inst = _test.double(DOUBLE_ME_PATH, DOUBLE_STRATEGY.FULL).new() + inst.get_instance_id() + assert_called(inst, 'get_instance_id') + + func test_strategy_used_with_scenes(): + var inst = _test.double(DOUBLE_ME_SCENE_PATH, DOUBLE_STRATEGY.FULL).instance() + inst.get_instance_id() + assert_called(inst, 'get_instance_id') + + func test_strategy_used_with_inners(): + var inst = _test.double(INNER_CLASSES_PATH, 'InnerA', DOUBLE_STRATEGY.FULL).new() + inst.get_instance_id() + assert_called(inst, 'get_instance_id') + + func test_when_passing_a_class_of_a_script_it_doubles_it(): + var inst = _test.double(DoubleMe).new() + assert_eq(inst.__gut_metadata_.path, DOUBLE_ME_PATH) + + func test_when_passing_a_class_of_a_scene_it_doubles_it(): + var inst = _test.double(DoubleMeScene).instance() + assert_eq(inst.__gut_metadata_.path, DOUBLE_ME_SCENE_PATH) + + func test_when_passing_a_class_of_an_inner_it_doubles_it(): + var inst = _test.double(InnerClasses, 'InnerA').new() + assert_eq(inst.__gut_metadata_.path, INNER_CLASSES_PATH, 'check path') + assert_eq(inst.__gut_metadata_.subpath, 'InnerA', 'check subpath') diff --git a/test/integration/test_this_scirpt_has_a_really_long_name_to_test_display.gd b/test/integration/test_this_script_has_a_really_long_name_to_test_display.gd similarity index 100% rename from test/integration/test_this_scirpt_has_a_really_long_name_to_test_display.gd rename to test/integration/test_this_script_has_a_really_long_name_to_test_display.gd diff --git a/test/parsing_and_loading_samples/has_inner_class.gd b/test/parsing_and_loading_samples/has_inner_class.gd index 03bd766a..78480dde 100644 --- a/test/parsing_and_loading_samples/has_inner_class.gd +++ b/test/parsing_and_loading_samples/has_inner_class.gd @@ -1,5 +1,5 @@ extends "res://addons/gut/test.gd" -func test_soemthing(): +func test_something(): pass func test_nothing(): diff --git a/test/parsing_and_loading_samples/inner_classes_check_before_after.gd b/test/parsing_and_loading_samples/inner_classes_check_before_after.gd new file mode 100644 index 00000000..df411c88 --- /dev/null +++ b/test/parsing_and_loading_samples/inner_classes_check_before_after.gd @@ -0,0 +1,39 @@ +# ############################################################################## +# These classes are used to verify that the befores and afters are being called +# correctly with inner classes. +# ############################################################################## +extends "res://addons/gut/test.gd" + +class BeforeAfterCounterTest: + extends "res://addons/gut/test.gd" + + var before_all_calls = 0 + var before_each_calls = 0 + var after_all_calls = 0 + var after_each_calls = 0 + + func before_all(): + before_all_calls += 1 + + func before_each(): + before_each_calls += 1 + + func after_all(): + after_all_calls += 1 + + func after_each(): + after_each_calls += 1 + + +class TestInner1: + extends BeforeAfterCounterTest + + func test_passing(): + assert_eq(1, 1, '1 = 1') + + +class TestInner2: + extends BeforeAfterCounterTest + + func test_passing(): + assert_eq(2, 2, '2 = 2') diff --git a/test/parsing_and_loading_samples/test_has_inner_class.gd b/test/parsing_and_loading_samples/test_has_inner_class.gd index e5f0f644..3e2985c4 100644 --- a/test/parsing_and_loading_samples/test_has_inner_class.gd +++ b/test/parsing_and_loading_samples/test_has_inner_class.gd @@ -8,7 +8,7 @@ func after_each(): func after_all(): gut.p('script: post-run') -func test_soemthing(): +func test_something(): assert_true(true) func test_nothing(): diff --git a/test/stub_test_objects/has_stub_metadata.gd b/test/stub_test_objects/has_stub_metadata.gd index 00ba2541..e58ac85d 100644 --- a/test/stub_test_objects/has_stub_metadata.gd +++ b/test/stub_test_objects/has_stub_metadata.gd @@ -1,3 +1,4 @@ var __gut_metadata_ = { - path = '' + path = '', + subpath='' } diff --git a/test/stub_test_objects/to_stub.gd b/test/stub_test_objects/to_stub.gd index d86cdbf9..5fb65767 100644 --- a/test/stub_test_objects/to_stub.gd +++ b/test/stub_test_objects/to_stub.gd @@ -1,3 +1,11 @@ +# this class is used by test_stubber and represents a doubled object +# which is why we have __gut_metadata_ in here. var value = 4 +var __gut_metadata_ = { + path='res://test/stub_test_objects/to_stub.gd', + subpath='', + stubber=null, + spy=null +} func get_value(): return value diff --git a/test/unit/test_doubler.gd b/test/unit/test_doubler.gd index fbf3bb63..664df204 100644 --- a/test/unit/test_doubler.gd +++ b/test/unit/test_doubler.gd @@ -1,108 +1,295 @@ extends "res://addons/gut/test.gd" -#var Doubler = load('res://addons/gut/doubler.gd') -const TEMP_FILES = 'user://test_doubler_temp_file' - -const DOUBLE_ME_PATH = 'res://test/doubler_test_objects/double_me.gd' -const DOUBLE_ME_SCENE_PATH = 'res://test/doubler_test_objects/double_me_scene.tscn' -const DOUBLE_EXTENDS_NODE2D = 'res://test/doubler_test_objects/double_extends_node2d.gd' -var Doubler = load('res://addons/gut/doubler.gd') -var gr = { - doubler = null -} - -func before_each(): - gr.doubler = Doubler.new() - gr.doubler.set_use_unique_names(false) - gr.doubler.set_output_dir(TEMP_FILES) - -func test_get_set_output_dir(): - assert_accessors(Doubler.new(), 'output_dir', null, 'somewhere') - -func test_get_set_stubber(): - assert_accessors(Doubler.new(), 'stubber', null, GDScript.new()) - -func test_can_get_set_spy(): - assert_accessors(Doubler.new(), 'spy', null, GDScript.new()) - -func test_setting_output_dir_creates_directory_if_it_does_not_exist(): - var d = Doubler.new() - d.set_output_dir('user://doubler_temp_files/') - var dir = Directory.new() - assert_true(dir.dir_exists('user://doubler_temp_files/')) - -func test_doubling_object_creates_temp_file(): - gr.doubler.double(DOUBLE_ME_PATH) - assert_file_exists(TEMP_FILES + '/double_me.gd') - -func test_doubling_object_includes_methods(): - gr.doubler.double(DOUBLE_ME_PATH) - var text = gut.get_file_as_text(TEMP_FILES.plus_file('double_me.gd')) - assert_true(text.match('*func get_value(*:\n*'), 'should have get method') - assert_true(text.match('*func set_value(*:\n*'), 'should have set method') - -func test_doubling_methods_have_parameters_1(): - gr.doubler.double(DOUBLE_ME_PATH) - var text = gut.get_file_as_text(TEMP_FILES.plus_file('double_me.gd')) - assert_true(text.match('*param(arg0*:*')) - -# Don't see a way to see which have defaults and which do not, so we default -# everything. -func test_all_parameters_are_defaulted_to_null(): - gr.doubler.double(DOUBLE_ME_PATH) - var text = gut.get_file_as_text(TEMP_FILES.plus_file('double_me.gd')) - assert_true(text.match('*one_default(arg0 = null, arg1 = null)*')) - -func test_doubled_thing_includes_stubber_metadata(): - var doubled = gr.doubler.double(DOUBLE_ME_PATH).new() - assert_ne(doubled.get('__gut_metadata_'), null) - -func test_doubled_thing_has_original_path_in_metadata(): - var doubled = gr.doubler.double(DOUBLE_ME_PATH).new() - assert_eq(doubled.__gut_metadata_.path, DOUBLE_ME_PATH) - -func test_keeps_extends(): - var doubled = gr.doubler.double(DOUBLE_EXTENDS_NODE2D).new() - assert_is(doubled, Node2D) - -func test_can_clear_output_directory(): - gr.doubler.double(DOUBLE_ME_PATH) - gr.doubler.double(DOUBLE_EXTENDS_NODE2D) - assert_file_exists(TEMP_FILES + '/double_me.gd') - assert_file_exists(TEMP_FILES + '/double_extends_node2d.gd') - gr.doubler.clear_output_directory() - assert_file_does_not_exist(TEMP_FILES + '/double_me.gd') - assert_file_does_not_exist(TEMP_FILES + '/double_extends_node2d.gd') - -func test_can_delete_output_directory(): - var d = Directory.new() - d.open('user://') - gr.doubler.double(DOUBLE_ME_PATH) - assert_true(d.dir_exists(TEMP_FILES)) - gr.doubler.delete_output_directory() - assert_false(d.dir_exists(TEMP_FILES)) - -func test_can_double_scene(): - var obj = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH) - var inst = obj.instance() - assert_eq(inst.return_hello(), null) - -func test_can_add_doubled_scene_to_tree(): - var inst = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() - add_child(inst) - assert_ne(inst.label, null) - remove_child(inst) - -func test_metadata_for_scenes_script_points_to_scene_not_script(): - var inst = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() - assert_eq(inst.__gut_metadata_.path, DOUBLE_ME_SCENE_PATH) - -func test_does_not_add_duplicate_methods(): - gr.doubler.double('res://test/parsing_and_loading_samples/extends_another_thing.gd') - assert_true(true, 'If we get here then the duplicates were removed.') - -# Keep this last so other tests fail before instantiation fails -func test_returns_class_that_can_be_instanced(): - var Doubled = gr.doubler.double(DOUBLE_ME_PATH) - var doubled = Doubled.new() - assert_ne(doubled, null) +class BaseTest: + extends "res://addons/gut/test.gd" + + #var Doubler = load('res://addons/gut/doubler.gd') + const TEMP_FILES = 'user://test_doubler_temp_file' + + const DOUBLE_ME_PATH = 'res://test/doubler_test_objects/double_me.gd' + const DOUBLE_ME_SCENE_PATH = 'res://test/doubler_test_objects/double_me_scene.tscn' + const DOUBLE_EXTENDS_NODE2D = 'res://test/doubler_test_objects/double_extends_node2d.gd' + const DOUBLE_EXTENDS_WINDOW_DIALOG = 'res://test/doubler_test_objects/double_extends_window_dialog.gd' + var Doubler = load('res://addons/gut/doubler.gd') + + func _get_temp_file_as_text(filename): + return gut.get_file_as_text(TEMP_FILES.plus_file(filename)) + +class TestTheBasics: + extends BaseTest + + var gr = { + doubler = null + } + + func before_each(): + gr.doubler = Doubler.new() + gr.doubler.set_use_unique_names(false) + gr.doubler.set_output_dir(TEMP_FILES) + + func after_each(): + gr.doubler.clear_output_directory() + + func test_get_set_output_dir(): + assert_accessors(Doubler.new(), 'output_dir', null, 'user://somewhere') + + func test_get_set_stubber(): + assert_accessors(Doubler.new(), 'stubber', null, GDScript.new()) + + func test_can_get_set_spy(): + assert_accessors(Doubler.new(), 'spy', null, GDScript.new()) + + func test_setting_output_dir_creates_directory_if_it_does_not_exist(): + var d = Doubler.new() + d.set_output_dir('user://doubler_temp_files/') + var dir = Directory.new() + assert_true(dir.dir_exists('user://doubler_temp_files/')) + + func test_doubling_object_creates_temp_file(): + gr.doubler.double(DOUBLE_ME_PATH) + assert_file_exists(TEMP_FILES + '/double_me.gd') + + func test_doubling_object_includes_methods(): + gr.doubler.double(DOUBLE_ME_PATH) + var text = gut.get_file_as_text(TEMP_FILES.plus_file('double_me.gd')) + assert_true(text.match('*func get_value(*:\n*'), 'should have get method') + assert_true(text.match('*func set_value(*:\n*'), 'should have set method') + + func test_doubling_methods_have_parameters_1(): + gr.doubler.double(DOUBLE_ME_PATH) + var text = gut.get_file_as_text(TEMP_FILES.plus_file('double_me.gd')) + assert_true(text.match('*param(p_arg0*:*'), text) + + # Don't see a way to see which have defaults and which do not, so we default + # everything. + func test_all_parameters_are_defaulted_to_null(): + gr.doubler.double(DOUBLE_ME_PATH) + var text = gut.get_file_as_text(TEMP_FILES.plus_file('double_me.gd')) + assert_true(text.match('*one_default(p_arg0=null, p_arg1=null)*')) + + func test_doubled_thing_includes_stubber_metadata(): + var doubled = gr.doubler.double(DOUBLE_ME_PATH).new() + assert_ne(doubled.get('__gut_metadata_'), null) + + func test_doubled_thing_has_original_path_in_metadata(): + var doubled = gr.doubler.double(DOUBLE_ME_PATH).new() + assert_eq(doubled.__gut_metadata_.path, DOUBLE_ME_PATH) + + func test_keeps_extends(): + var doubled = gr.doubler.double(DOUBLE_EXTENDS_NODE2D).new() + assert_is(doubled, Node2D) + + func test_can_clear_output_directory(): + gr.doubler.double(DOUBLE_ME_PATH) + gr.doubler.double(DOUBLE_EXTENDS_NODE2D) + assert_file_exists(TEMP_FILES + '/double_me.gd') + assert_file_exists(TEMP_FILES + '/double_extends_node2d.gd') + gr.doubler.clear_output_directory() + assert_file_does_not_exist(TEMP_FILES + '/double_me.gd') + assert_file_does_not_exist(TEMP_FILES + '/double_extends_node2d.gd') + + func test_can_delete_output_directory(): + var d = Directory.new() + d.open('user://') + gr.doubler.double(DOUBLE_ME_PATH) + assert_true(d.dir_exists(TEMP_FILES)) + gr.doubler.delete_output_directory() + assert_false(d.dir_exists(TEMP_FILES)) + + func test_can_double_scene(): + var obj = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH) + var inst = obj.instance() + assert_eq(inst.return_hello(), null) + + func test_can_add_doubled_scene_to_tree(): + var inst = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() + add_child(inst) + assert_ne(inst.label, null) + remove_child(inst) + #print(gr.doubler.get_spy().get_call_list_as_string(inst)) + + func test_metadata_for_scenes_script_points_to_scene_not_script(): + var inst = gr.doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() + assert_eq(inst.__gut_metadata_.path, DOUBLE_ME_SCENE_PATH) + + func test_does_not_add_duplicate_methods(): + gr.doubler.double('res://test/parsing_and_loading_samples/extends_another_thing.gd') + assert_true(true, 'If we get here then the duplicates were removed.') + + # Keep this last so other tests fail before instantiation fails + func test_returns_class_that_can_be_instanced(): + var Doubled = gr.doubler.double(DOUBLE_ME_PATH) + var doubled = Doubled.new() + assert_ne(doubled, null) + + func test_get_set_logger(): + assert_ne(gr.doubler.get_logger(), null) + var l = load('res://addons/gut/logger.gd').new() + gr.doubler.set_logger(l) + assert_eq(gr.doubler.get_logger(), l) + + func test_doubler_sets_logger_of_method_maker(): + assert_eq(gr.doubler.get_logger(), gr.doubler._method_maker.get_logger()) + + func test_setting_logger_sets_it_on_method_maker(): + var l = load('res://addons/gut/logger.gd').new() + gr.doubler.set_logger(l) + assert_eq(gr.doubler.get_logger(), gr.doubler._method_maker.get_logger()) + + func test_get_set_strategy(): + assert_accessors(gr.doubler, 'strategy', _utils.DOUBLE_STRATEGY.PARTIAL, _utils.DOUBLE_STRATEGY.FULL) + + func test_can_set_strategy_in_constructor(): + var d = Doubler.new(_utils.DOUBLE_STRATEGY.FULL) + assert_eq(d.get_strategy(), _utils.DOUBLE_STRATEGY.FULL) + +class TestBuiltInOverloading: + extends BaseTest + + var _dbl_win_dia_text = '' + + func _hide_call_back(): + pass + + var doubler = null + func before_each(): + doubler = Doubler.new(_utils.DOUBLE_STRATEGY.FULL) + doubler.set_use_unique_names(false) + doubler.set_output_dir(TEMP_FILES) + + # WindowDialog has A LOT of the edge cases we need to check so it is used + # as the default. + doubler.double(DOUBLE_EXTENDS_WINDOW_DIALOG) + _dbl_win_dia_text = _get_temp_file_as_text('double_extends_window_dialog.gd') + + func after_all(): + pass#doubler.clear_output_directory() + + func test_built_in_overloading_ony_happens_on_full_strategy(): + doubler.set_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) + doubler.double(DOUBLE_ME_PATH) + var txt = _get_temp_file_as_text('double_me.gd') + assert_eq(txt.find('func is_blocking_signals'), -1, 'does not have non-overloaded methods') + + func test_can_override_strategy_when_doubling_script(): + doubler.set_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) + doubler.double(DOUBLE_ME_PATH, DOUBLE_STRATEGY.FULL) + var txt = _get_temp_file_as_text('double_me.gd') + assert_ne(txt.find('func is_blocking_signals'), -1, 'HAS non-overloaded methods') + + func test_can_override_strategy_when_doubling_scene(): + doubler.set_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) + doubler.double_scene(DOUBLE_ME_SCENE_PATH, DOUBLE_STRATEGY.FULL) + var txt = _get_temp_file_as_text('double_me_scene.gd') + assert_ne(txt.find('func is_blocking_signals'), -1, 'HAS non-overloaded methods') + + func test_when_everything_included_you_can_still_make_an_a_new_object(): + var inst = doubler.double(DOUBLE_ME_PATH).new() + assert_ne(inst, null) + + func test_when_everything_included_you_can_still_make_a_new_node2d(): + var inst = doubler.double(DOUBLE_EXTENDS_NODE2D).new() + assert_ne(inst, null) + + func test_when_everything_included_you_can_still_double_a_scene(): + var inst = doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() + add_child(inst) + assert_ne(inst, null, "instance is not null") + assert_ne(inst.label, null, "Can get to a label on the instance") + # pause so _process gets called + yield(yield_for(3), YIELD) + end_test() + + func test_double_includes_methods_in_super(): + assert_string_contains(_dbl_win_dia_text, 'connect(') + + func test_can_call_a_built_in_that_has_default_parameters(): + var inst = doubler.double(DOUBLE_EXTENDS_WINDOW_DIALOG).new() + inst.connect('hide', self, '_hide_call_back') + + func test_all_types_supported(): + assert_string_contains(_dbl_win_dia_text, 'popup_centered(p_size=Vector2(0, 0)):', 'Vector2') + assert_string_contains(_dbl_win_dia_text, 'bounds=Rect2(0, 0, 0, 0)', 'Rect2') + + func test_doubled_builtins_call_super(): + var inst = doubler.double(DOUBLE_EXTENDS_WINDOW_DIALOG).new() + # Make sure the function is in the doubled class definition + assert_string_contains(_dbl_win_dia_text, 'func add_user_signal(p_signal') + # Make sure that when called it retains old functionality. + inst.add_user_signal('new_one') + inst.add_user_signal('new_two', ['a', 'b']) + assert_has_signal(inst, 'new_one') + assert_has_signal(inst, 'new_two') + +# Since defaults are only available for built-in methods these tests verify +# specific method parameters that were found to cause a problem. +class TestDefaultParameters: + extends BaseTest + + var doubler = null + + func before_each(): + doubler = Doubler.new(_utils.DOUBLE_STRATEGY.FULL) + doubler.set_use_unique_names(false) + doubler.set_output_dir(TEMP_FILES) + + func test_parameters_are_doubled_for_connect(): + var inst = doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() + var text = _get_temp_file_as_text('double_me_scene.gd') + var sig = 'func connect(p_signal=null, p_target=null, p_method=null, p_binds=[], p_flags=0):' + assert_string_contains(text, sig) + + func test_parameters_are_doubled_for_draw_char(): + var inst = doubler.double_scene(DOUBLE_ME_SCENE_PATH).instance() + var text = _get_temp_file_as_text('double_me_scene.gd') + var sig = 'func draw_char(p_font=null, p_position=null, p_char=null, p_next=null, p_modulate=Color(1,1,1,1)):' + assert_string_contains(text, sig) + +class TestDoubleInnerClasses: + extends BaseTest + + var doubler = null + const INNER_CLASSES_PATH = 'res://test/doubler_test_objects/inner_classes.gd' + var InnerClasses = load(INNER_CLASSES_PATH) + + func before_each(): + doubler = Doubler.new() + doubler.set_use_unique_names(false) + doubler.set_output_dir(TEMP_FILES) + + func test_can_instantiate_inner_double(): + var Doubled = doubler.double_inner(INNER_CLASSES_PATH, 'InnerB/InnerB1') + assert_has_method(Doubled.new(), 'get_b1') + + func test_doubled_instance_returns_null_for_get_b1(): + var dbld = doubler.double_inner(INNER_CLASSES_PATH, 'InnerB/InnerB1').new() + assert_null(dbld.get_b1()) + + func test_double_file_contains_source_file_and_inner_classes_in_the_name(): + doubler.double_inner(INNER_CLASSES_PATH, 'InnerB/InnerB1') + assert_file_exists(TEMP_FILES + '/inner_classes__InnerB__InnerB1.gd') + + func test_doubled_instances_extend_the_inner_class(): + var inst = doubler.double_inner(INNER_CLASSES_PATH, 'InnerA').new() + assert_extends(inst, InnerClasses.InnerA) + + func test_doubled_inners_that_extend_inners_get_full_inheritance(): + var inst = doubler.double_inner(INNER_CLASSES_PATH, 'InnerCA').new() + assert_has_method(inst, 'get_a') + assert_has_method(inst, 'get_ca') + + func test_doubled_inners_have_subpath_set_in_metadata(): + var inst = doubler.double_inner(INNER_CLASSES_PATH, 'InnerCA').new() + assert_eq(inst.__gut_metadata_.subpath, 'InnerCA') + + func test_non_inners_have_empty_subpath(): + var inst = doubler.double(INNER_CLASSES_PATH).new() + assert_eq(inst.__gut_metadata_.subpath, '') + + func test_can_override_strategy_when_doubling(): + #doubler.set_strategy(DOUBLE_STRATEGY.FULL) + var d = doubler.double_inner(INNER_CLASSES_PATH, 'InnerA', DOUBLE_STRATEGY.FULL) + var text = _get_temp_file_as_text('inner_classes__InnerA.gd') + # make sure it has something from Object that isn't implemented + assert_string_contains(text, 'func disconnect(p_signal') + assert_eq(doubler.get_strategy(), DOUBLE_STRATEGY.PARTIAL, 'strategy should have been reset') diff --git a/test/unit/test_gut.gd b/test/unit/test_gut.gd index 4cce1b28..df53ec65 100644 --- a/test/unit/test_gut.gd +++ b/test/unit/test_gut.gd @@ -143,6 +143,22 @@ func test_get_current_script_object_returns_null_by_default(): func test_get_set_temp_directory(): assert_accessors(gr.test_gut, 'temp_directory', 'user://gut_temp_directory', 'user://blahblah') +# ------------------------------ +# Double Strategy +# ------------------------------ +func test_get_set_double_strategy(): + assert_accessors(gr.test_gut, 'double_strategy', 1, 2) + +func test_when_test_overrides_strategy_it_is_reset_after_test_finishes(): + gr.test_gut.set_double_strategy(_utils.DOUBLE_STRATEGY.PARTIAL) + gr.test_gut.add_script('res://test/samples/test_before_after.gd') + gr.test_gut.get_doubler().set_strategy(_utils.DOUBLE_STRATEGY.FULL) + gr.test_gut.test_scripts() + assert_eq(gr.test_gut.get_double_strategy(), _utils.DOUBLE_STRATEGY.PARTIAL) + + + + # ------------------------------ # disable strict datatype comparisons # ------------------------------ @@ -330,6 +346,26 @@ func test_after_running_script_everything_checks_out(): assert_eq(instance.counts.postrun_teardown, 1, 'postrun_teardown') assert_eq(instance.counts.teardown, 3, 'teardown') +func test_when_inner_class_skipped_none_of_the_before_after_are_called(): + gr.test_gut.add_script('res://test/parsing_and_loading_samples/inner_classes_check_before_after.gd') + gr.test_gut.set_inner_class_name('Inner1') + gr.test_gut.test_scripts() + var instances = gr.test_gut._test_script_objects + + # instances[0] is the outer script + + assert_eq(instances[1].before_all_calls, 1, 'TestInner1 before_all calls') + assert_eq(instances[1].after_all_calls, 1, 'TestInner1 after_all calls') + assert_eq(instances[1].before_each_calls, 1, 'TestInner1 before_each_calls') + assert_eq(instances[1].after_each_calls, 1, 'TestInner1 after_each calls') + + assert_eq(instances[2].before_all_calls, 0, 'TestInner2 before_all calls') + assert_eq(instances[2].after_all_calls, 0, 'TestInner2 after_all calls') + assert_eq(instances[2].before_each_calls, 0, 'TestInner2 before_each_calls') + assert_eq(instances[2].after_each_calls, 0, 'TestInner2 after_each calls') + + + # ------------------------------------------------------------------------------ # diff --git a/test/unit/test_gut_directory.gd b/test/unit/test_gut_directory.gd index 90f375c4..3271246f 100644 --- a/test/unit/test_gut_directory.gd +++ b/test/unit/test_gut_directory.gd @@ -151,10 +151,12 @@ class TestUsingDynamicDirs: var dir = Directory.new() var i = _test_dirs.size() -1 # delete the directories in reverse order since it is easier than - # recursively deleting a directory and everyting in it. + # recursively deleting a directory and everything in it. while(i > 0): gut.directory_delete_files(_test_dirs[i]) - dir.remove(_test_dirs[i]) + var result = dir.open(_test_dirs[i]) + if(result == OK): + dir.remove(_test_dirs[i]) i -= 1 _test_dirs.clear() diff --git a/test/unit/test_method_maker.gd b/test/unit/test_method_maker.gd new file mode 100644 index 00000000..c2a1dac2 --- /dev/null +++ b/test/unit/test_method_maker.gd @@ -0,0 +1,125 @@ +extends "res://addons/gut/test.gd" + + +class BaseTest: + extends "res://addons/gut/test.gd" + + var MethodMaker = load('res://addons/gut/method_maker.gd') + + func make_meta(fname, params = [], flags = 65): + var to_return = { + name = fname, + args = params, + default_args = [] + } + return to_return + + func make_param(pname, ptype): + var to_return = { + name = pname, + type = ptype + } + return to_return + +class TestGetDecleration: + extends BaseTest + + var _mm = null + + func before_each(): + _mm = MethodMaker.new() + + func test_get_decleration_text_no_params(): + assert_eq(_mm.get_decleration_text(make_meta('dummy')), 'func dummy():') + + func test_if_unknown_param_type_specified_it_does_not_blow_up(): + var params = [make_param('value1', 999)] + var meta = make_meta('dummy', params) + meta.default_args.append(1) + var txt = _mm.get_decleration_text(meta) + assert_true(true, 'we got here') + + func test_if_unknonw_param_type_function_text_is_null(): + var params = [make_param('value1', 999)] + var meta = make_meta('dummy', params) + meta.default_args.append(1) + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, null) + + func test_parameters_get_prefix_and_default_null(): + var params = [make_param('value1', TYPE_INT), make_param('value2', TYPE_INT)] + var meta = make_meta('dummy', params) + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=null, p_value2=null):') + + func test_default_only_param(): + var params = [make_param('value1', TYPE_INT)] + var meta = make_meta('dummy', params) + meta.default_args.append(1) + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=1):') + + func test_default_2nd_param(): + var params = [make_param('value1', TYPE_INT), make_param('value2', TYPE_INT)] + var meta = make_meta('dummy', params) + meta.default_args.append(1) + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=null, p_value2=1):') + + func test_vector2_default(): + var params = [make_param('value1', TYPE_VECTOR2)] + var meta = make_meta('dummy', params) + meta.default_args.append('(0,0)') + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=Vector2(0,0)):') + + func test_rect2_default(): + var params = [make_param('value1', TYPE_RECT2)] + var meta = make_meta('dummy', params) + meta.default_args.append('(0,0,0,0)') + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=Rect2(0,0,0,0)):') + + func test_string_default(): + var params = [make_param('value1', TYPE_STRING)] + var meta = make_meta('dummy', params) + meta.default_args.append('aSDf') + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=\'aSDf\'):') + + func test_vector3_default(): + var params = [make_param('value1', TYPE_VECTOR3)] + var meta = make_meta('dummy', params) + meta.default_args.append('(0,0,0)') + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=Vector3(0,0,0)):') + + func test_color_default(): + var params = [make_param('value1', TYPE_COLOR)] + var meta = make_meta('dummy', params) + meta.default_args.append('1,1,1,1') + var txt = _mm.get_decleration_text(meta) + assert_eq(txt, 'func dummy(p_value1=Color(1,1,1,1)):') + +class TestSuperCall: + extends BaseTest + + var _mm = null + + func before_each(): + _mm = MethodMaker.new() + + func test_super_call_works_with_no_parameters(): + var meta = make_meta('dummy') + var text = _mm.get_super_call_text(meta) + assert_eq(text, '.dummy()') + + func test_super_call_contains_all_parameters(): + var params = [ + make_param('value1', TYPE_COLOR), + make_param('value2', TYPE_INT), + make_param('value3', TYPE_STRING) + ] + var meta = make_meta('dummy', params) + var text = _mm.get_super_call_text(meta) + assert_eq(text, '.dummy(p_value1, p_value2, p_value3)') diff --git a/test/unit/test_signal_watcher.gd b/test/unit/test_signal_watcher.gd index 623d2292..05feaa4b 100644 --- a/test/unit/test_signal_watcher.gd +++ b/test/unit/test_signal_watcher.gd @@ -1,7 +1,7 @@ extends "res://addons/gut/test.gd" # ############################################################################## # Some notes about signals: -# * The paramters appear to be completely optional when defined via +# * The parameters appear to be completely optional when defined via # add_user_signal. # * As long as you have enough parameters defined (defaulted of course) on # your handler, then it be notified of the signal when it is emitted. diff --git a/test/unit/test_stub_params.gd b/test/unit/test_stub_params.gd index 3208157d..10bc5d2b 100644 --- a/test/unit/test_stub_params.gd +++ b/test/unit/test_stub_params.gd @@ -24,6 +24,10 @@ func test_init_sets_stub_target(): var s = StubParamsClass.new('thing') assert_eq(s.stub_target, 'thing') +func test_init_sets_subpath(): + var s = StubParamsClass.new('thing', 'method', 'inner1/inner2') + assert_eq(s.target_subpath, 'inner1/inner2') + func test_init_sets_method(): var s = StubParamsClass.new('thing', 'method') assert_eq(s.stub_method, 'method') diff --git a/test/unit/test_stubber.gd b/test/unit/test_stubber.gd index 382302e0..d4e81d1a 100644 --- a/test/unit/test_stubber.gd +++ b/test/unit/test_stubber.gd @@ -12,11 +12,8 @@ var ToStub = load(TO_STUB_PATH) const HAS_STUB_METADATA_PATH = 'res://test/stub_test_objects/has_stub_metadata.gd' var HasStubMetadata = load(HAS_STUB_METADATA_PATH) - -class InnerClass: - func return_seven(): - return 7 - +const INNER_CLASSES_PATH = 'res://test/doubler_test_objects/inner_classes.gd' +var InnerClasses = load(INNER_CLASSES_PATH) var gr = { stubber = null @@ -162,8 +159,3 @@ func test_withStubParams_param_layering_works(): assert_eq(sp1_r, 10, 'When passed 10 it gets 10') assert_eq(sp2_r, 5, 'When passed 5 it gets 5') assert_eq(sp3_r, 'nothing', 'When params do not match it sends default back.') -func test_inners(): - # this only works with instances but we can get to the path and all the - # neat stuff, this might be the path to stubbing inners - var i = InnerClass.new() - print(inst2dict(i)) diff --git a/test/unit/test_test.gd b/test/unit/test_test.gd index 9c3daaf5..f7985f30 100644 --- a/test/unit/test_test.gd +++ b/test/unit/test_test.gd @@ -13,10 +13,7 @@ extends "res://addons/gut/test.gd" class BaseTestClass: - extends "res://addons/gut/test.gd" - - var Gut = load('res://addons/gut/gut.gd') - var Test = load('res://addons/gut/test.gd') + extends "res://test/gut_test.gd" # !! Use this for debugging to see the results of all the subtests that # are run using assert_fail_pass, assert_fail and assert_pass that are # built into this class @@ -255,7 +252,7 @@ class TestAssertGt: assert_pass(gr.test) func test_fails_with_less_than_string(): - gr.test.assert_gt("a", "b", "Sould Fail") + gr.test.assert_gt("a", "b", "Should Fail") assert_fail(gr.test) # TODO rename tests since they are now in an inner class. See NOTE at top about naming. @@ -743,17 +740,17 @@ class TestSignalAsserts: gr.signal_object.emit_signal(SIGNALS.SOME_SIGNAL) assert_eq(gr.test.get_signal_emit_count(gr.signal_object, SIGNALS.SOME_SIGNAL), 2) - func test__assert_signal_emitted_with_paramters__fails_when_object_not_watched(): + func test__assert_signal_emitted_with_parameters__fails_when_object_not_watched(): gr.test.assert_signal_emitted_with_parameters(gr.signal_object, SIGNALS.SOME_SIGNAL, []) assert_fail(gr.test) - func test__assert_signal_emitted_with_parameters__passes_when_paramters_match(): + func test__assert_signal_emitted_with_parameters__passes_when_parameters_match(): gr.test.watch_signals(gr.signal_object) gr.signal_object.emit_signal(SIGNALS.SOME_SIGNAL, 1) gr.test.assert_signal_emitted_with_parameters(gr.signal_object, SIGNALS.SOME_SIGNAL, [1]) assert_pass(gr.test) - func test__assert_signal_emitted_with_parameters__passes_when_all_paramters_match(): + func test__assert_signal_emitted_with_parameters__passes_when_all_parameters_match(): gr.test.watch_signals(gr.signal_object) gr.signal_object.emit_signal(SIGNALS.SOME_SIGNAL, 1, 2, 3) gr.test.assert_signal_emitted_with_parameters(gr.signal_object, SIGNALS.SOME_SIGNAL, [1, 2, 3]) @@ -764,13 +761,13 @@ class TestSignalAsserts: gr.test.assert_signal_emitted_with_parameters(gr.signal_object, SIGNALS.SOME_SIGNAL, [2]) assert_fail(gr.test) - func test__assert_signal_emitted_with_parameters__fails_when_paramters_dont_match(): + func test__assert_signal_emitted_with_parameters__fails_when_parameters_dont_match(): gr.test.watch_signals(gr.signal_object) gr.signal_object.emit_signal(SIGNALS.SOME_SIGNAL, 1) gr.test.assert_signal_emitted_with_parameters(gr.signal_object, SIGNALS.SOME_SIGNAL, [2]) assert_fail(gr.test) - func test__assert_signal_emitted_with_parameters__fails_when_not_all_paramters_match(): + func test__assert_signal_emitted_with_parameters__fails_when_not_all_parameters_match(): gr.test.watch_signals(gr.signal_object) gr.signal_object.emit_signal(SIGNALS.SOME_SIGNAL, 1, 2, 3) gr.test.assert_signal_emitted_with_parameters(gr.signal_object, SIGNALS.SOME_SIGNAL, [1, 0, 3]) @@ -914,7 +911,7 @@ class TestStringContains: gr.test.assert_string_contains('This is a test.', 'is a ', true) assert_pass(gr.test) - func test__assert_string_contains__passes_when_case_insensitve_search_is_found(): + func test__assert_string_contains__passes_when_case_insensitive_search_is_found(): gr.test.assert_string_contains('This is a test.', 'this ', false) assert_pass(gr.test) @@ -987,7 +984,7 @@ class TestAssertCalled: gut.p('!! Check output !!') assert_fail(gr.test_with_gut) - func test_assert_called_passes_when_call_occured(): + func test_assert_called_passes_when_call_occurred(): var doubled = gr.test_with_gut.double(DOUBLE_ME_PATH).new() doubled.get_value() gr.test_with_gut.assert_called(doubled, 'get_value') @@ -1096,3 +1093,49 @@ class TestAssertCallCount: doubled.set_value(12) gr.test_with_gut.assert_call_count(doubled, 'set_value', 4) assert_pass(gr.test_with_gut) + +class TestAssertNull: + extends BaseTestClass + + func test_when_null_assert_passes(): + gr.test.assert_null(null) + assert_pass(gr.test) + + func test_when_not_null_assert_fails(): + gr.test.assert_null('a') + assert_fail(gr.test) + + func test_accepts_text(): + gr.test.assert_null('a', 'a is not null') + assert_fail(gr.test) + + func test_does_not_blow_up_on_different_kinds_of_input(): + gr.test.assert_null(Node2D.new()) + gr.test.assert_null(1) + gr.test.assert_null([]) + gr.test.assert_null({}) + gr.test.assert_null(Color(1,1,1,1)) + assert_fail(gr.test, 5) + +class TestAssertNotNull: + extends BaseTestClass + + func test_when_null_assert_fails(): + gr.test.assert_not_null(null) + assert_fail(gr.test) + + func test_when_not_null_assert_passes(): + gr.test.assert_not_null('a') + assert_pass(gr.test) + + func test_accepts_text(): + gr.test.assert_not_null('a', 'a is not null') + assert_pass(gr.test) + + func test_does_not_blow_up_on_different_kinds_of_input(): + gr.test.assert_not_null(Node2D.new()) + gr.test.assert_not_null(1) + gr.test.assert_not_null([]) + gr.test.assert_not_null({}) + gr.test.assert_not_null(Color(1,1,1,1)) + assert_pass(gr.test, 5) diff --git a/test/unit/test_test_collector.gd b/test/unit/test_test_collector.gd index bfcd1e86..b547e259 100644 --- a/test/unit/test_test_collector.gd +++ b/test/unit/test_test_collector.gd @@ -19,7 +19,7 @@ func test_does_not_have_not_prefixed(): assert_ne(gr.tc.scripts[0].tests[i].name, 'not_prefixed') func test_get_set_test_prefix(): - assert_accessors(gr.tc, 'test_prefix', 'test_', 'soemthing') + assert_accessors(gr.tc, 'test_prefix', 'test_', 'something') func test_can_change_test_prefix(): gr.tc.set_test_prefix('diff_prefix_')