taqin/addons/gut/gut.gd

1335 lines
50 KiB
GDScript

################################################################################
#(G)odot (U)nit (T)est class
#
################################################################################
#The MIT License (MIT)
#=====================
#
#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
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
################################################################################
# View readme for usage details.
#
# Version 6.8.1
################################################################################
#extends "res://addons/gut/gut_gui.gd"
tool
extends Control
var _version = '6.8.1'
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
# Used to prevent multiple messages for deprecated setup/teardown messages
var _deprecated_tracker = _utils.ThingCounter.new()
# ###########################
# Editor Variables
# ###########################
export(String) var _select_script = ''
export(String) var _tests_like = ''
export(String) var _inner_class_name = ''
export var _run_on_load = false
export var _should_maximize = false setget set_should_maximize, get_should_maximize
export var _should_print_to_console = true setget set_should_print_to_console, get_should_print_to_console
export(int, 'Failures only', 'Tests and failures', 'Everything') var _log_level = 1 setget set_log_level, get_log_level
# This var is JUST used to expose this setting in the editor
# the var that is used is in the _yield_between hash.
export var _yield_between_tests = true setget set_yield_between_tests, get_yield_between_tests
export var _disable_strict_datatype_checks = false setget disable_strict_datatype_checks, is_strict_datatype_checks_disabled
# The prefix used to get tests.
export var _test_prefix = 'test_'
export var _file_prefix = 'test_'
export var _file_extension = '.gd'
export var _inner_class_prefix = 'Test'
export(String) var _temp_directory = 'user://gut_temp_directory'
export(String) var _export_path = '' setget set_export_path, get_export_path
export var _include_subdirectories = false setget set_include_subdirectories, get_include_subdirectories
# 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
# cumbersome and complicates testing because arrays set through the editor
# apply to ALL instances. This also allows the user to use the built in
# dialog to pick a directory.
export(String, DIR) var _directory1 = ''
export(String, DIR) var _directory2 = ''
export(String, DIR) var _directory3 = ''
export(String, DIR) var _directory4 = ''
export(String, DIR) var _directory5 = ''
export(String, DIR) var _directory6 = ''
export(int, 'FULL', 'PARTIAL') var _double_strategy = _utils.DOUBLE_STRATEGY.PARTIAL setget set_double_strategy, get_double_strategy
export(String, FILE) var _pre_run_script = '' setget set_pre_run_script, get_pre_run_script
export(String, FILE) var _post_run_script = '' setget set_post_run_script, get_post_run_script
# The instance that is created from _pre_run_script. Accessible from
# get_pre_run_script_instance.
var _pre_run_script_instance = null
var _post_run_script_instance = null # This is not used except in tests.
# ###########################
# Other Vars
# ###########################
const LOG_LEVEL_FAIL_ONLY = 0
const LOG_LEVEL_TEST_AND_FAILURES = 1
const LOG_LEVEL_ALL_ASSERTS = 2
const WAITING_MESSAGE = '/# waiting #/'
const PAUSE_MESSAGE = '/# Pausing. Press continue button...#/'
var _script_name = null
var _test_collector = _utils.TestCollector.new()
# The instanced scripts. This is populated as the scripts are run.
var _test_script_objects = []
var _waiting = false
var _done = false
var _is_running = false
var _current_test = null
var _log_text = ""
var _pause_before_teardown = false
# when true _pause_before_teardown will be ignored. useful
# when batch processing and you don't want to watch.
var _ignore_pause_before_teardown = false
var _wait_timer = Timer.new()
var _yield_between = {
should = false,
timer = Timer.new(),
after_x_tests = 5,
tests_since_last_yield = 0
}
var _was_yield_method_called = false
# used when yielding to gut instead of some other
# signal. Start with set_yield_time()
var _yield_timer = Timer.new()
var _unit_test_name = ''
var _new_summary = null
var _yielding_to = {
obj = null,
signal_name = ''
}
var _stubber = _utils.Stubber.new()
var _doubler = _utils.Doubler.new()
var _spy = _utils.Spy.new()
var _gui = null
const SIGNAL_TESTS_FINISHED = 'tests_finished'
const SIGNAL_STOP_YIELD_BEFORE_TEARDOWN = 'stop_yield_before_teardown'
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _init():
# This min size has to be what the min size of the GutScene's min size is
# but it has to be set here and not inferred i think.
rect_min_size =Vector2(740, 250)
add_user_signal(SIGNAL_TESTS_FINISHED)
add_user_signal(SIGNAL_STOP_YIELD_BEFORE_TEARDOWN)
add_user_signal('timeout')
add_user_signal('done_waiting')
_doubler.set_output_dir(_temp_directory)
_doubler.set_stubber(_stubber)
_doubler.set_spy(_spy)
_doubler.set_logger(_lgr)
_lgr.set_gut(self)
_stubber.set_logger(_lgr)
_test_collector.set_logger(_lgr)
_gui = load('res://addons/gut/GutScene.tscn').instance()
# ------------------------------------------------------------------------------
# Initialize controls
# ------------------------------------------------------------------------------
func _ready():
_lgr.info(str('using [', OS.get_user_data_dir(), '] for temporary output.'))
set_process_input(true)
add_child(_wait_timer)
_wait_timer.set_wait_time(1)
_wait_timer.set_one_shot(true)
add_child(_yield_between.timer)
_wait_timer.set_one_shot(true)
add_child(_yield_timer)
_yield_timer.set_one_shot(true)
_yield_timer.connect('timeout', self, '_yielding_callback')
_setup_gui()
add_directory(_directory1)
add_directory(_directory2)
add_directory(_directory3)
add_directory(_directory4)
add_directory(_directory5)
add_directory(_directory6)
if(_select_script != null):
select_script(_select_script)
if(_tests_like != null):
set_unit_test_name(_tests_like)
if(_run_on_load):
test_scripts(_select_script == null)
if(_should_maximize):
maximize()
# hide the panel that IS gut so that only the GUI is seen
self.self_modulate = Color(1,1,1,0)
show()
var v_info = Engine.get_version_info()
p(str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch))
p(str('GUT version: ', get_version()))
################################################################################
#
# GUI Events and setup
#
################################################################################
func _setup_gui():
# This is how we get the size of the control to translate to the gui when
# the scene is run. This is also another reason why the min_rect_size
# must match between both gut and the gui.
_gui.rect_size = self.rect_size
add_child(_gui)
_gui.set_anchor(MARGIN_RIGHT, ANCHOR_END)
_gui.set_anchor(MARGIN_BOTTOM, ANCHOR_END)
_gui.connect('run_single_script', self, '_on_run_one')
_gui.connect('run_script', self, '_on_new_gui_run_script')
_gui.connect('end_pause', self, '_on_new_gui_end_pause')
_gui.connect('ignore_pause', self, '_on_new_gui_ignore_pause')
_gui.connect('log_level_changed', self, '_on_log_level_changed')
connect('tests_finished', _gui, 'end_run')
func _add_scripts_to_gui():
var scripts = []
for i in range(_test_collector.scripts.size()):
var s = _test_collector.scripts[i]
var txt = ''
if(s.has_inner_class()):
txt = str(' - ', s.inner_class_name, ' (', s.tests.size(), ')')
else:
txt = str(s.get_full_name(), ' (', s.tests.size(), ')')
scripts.append(txt)
_gui.set_scripts(scripts)
func _on_run_one(index):
clear_text()
var indexes = [index]
if(!_test_collector.scripts[index].has_inner_class()):
indexes = _get_indexes_matching_path(_test_collector.scripts[index].path)
_test_the_scripts(indexes)
func _on_new_gui_run_script(index):
var indexes = []
clear_text()
for i in range(index, _test_collector.scripts.size()):
indexes.append(i)
_test_the_scripts(indexes)
func _on_new_gui_end_pause():
_pause_before_teardown = false
emit_signal(SIGNAL_STOP_YIELD_BEFORE_TEARDOWN)
func _on_new_gui_ignore_pause(should):
_ignore_pause_before_teardown = should
func _on_log_level_changed(value):
_log_level = value
#####################
#
# Events
#
#####################
# ------------------------------------------------------------------------------
# Timeout for the built in timer. emits the timeout signal. Start timer
# with set_yield_time()
# ------------------------------------------------------------------------------
func _yielding_callback(from_obj=false):
if(_yielding_to.obj):
_yielding_to.obj.call_deferred(
"disconnect",
_yielding_to.signal_name, self,
'_yielding_callback')
_yielding_to.obj = null
_yielding_to.signal_name = ''
if(from_obj):
# we must yiled for a little longer after the signal is emitted so that
# the signal can propagate to other objects. This was discovered trying
# to assert that obj/signal_name was emitted. Without this extra delay
# the yield returns and processing finishes before the rest of the
# objects can get the signal. This works b/c the timer will timeout
# and come back into this method but from_obj will be false.
_yield_timer.set_wait_time(.1)
_yield_timer.start()
else:
emit_signal('timeout')
# ------------------------------------------------------------------------------
# completed signal for GDScriptFucntionState returned from a test script that
# has yielded
# ------------------------------------------------------------------------------
func _on_test_script_yield_completed():
_waiting = false
#####################
#
# Private
#
#####################
# ------------------------------------------------------------------------------
# Convert the _summary dictionary into text
# ------------------------------------------------------------------------------
func _get_summary_text():
var to_return = "\n\n*****************\nRun Summary\n*****************"
to_return += "\n" + _new_summary.get_summary_text() + "\n"
var logger_text = ''
if(_lgr.get_errors().size() > 0):
logger_text += str("\n * ", _lgr.get_errors().size(), ' Errors.')
if(_lgr.get_warnings().size() > 0):
logger_text += str("\n * ", _lgr.get_warnings().size(), ' Warnings.')
if(_lgr.get_deprecated().size() > 0):
logger_text += str("\n * ", _lgr.get_deprecated().size(), ' Deprecated calls.')
if(logger_text != ''):
logger_text = "\nWarnings/Errors:" + logger_text + "\n\n"
to_return += logger_text
if(_new_summary.get_totals().tests > 0):
to_return += '+++ ' + str(_new_summary.get_totals().passing) + ' passed ' + str(_new_summary.get_totals().failing) + ' failed. ' + \
"Tests finished in: " + str(_gui.get_run_duration()) + ' +++'
var c = Color(0, 1, 0)
if(_new_summary.get_totals().failing > 0):
c = Color(1, 0, 0)
elif(_new_summary.get_totals().pending > 0):
c = Color(1, 1, .8)
_gui.get_text_box().add_color_region('+++', '+++', c)
else:
to_return += '+++ No tests ran +++'
_gui.get_text_box().add_color_region('+++', '+++', Color(1, 0, 0))
return to_return
func _validate_hook_script(path):
var result = {
valid = true,
instance = null
}
# empty path is valid but will have a null instance
if(path == ''):
return result
var f = File.new()
if(f.file_exists(path)):
var inst = load(path).new()
if(inst and inst is _utils.HookScript):
result.instance = inst
result.valid = true
else:
result.valid = false
_lgr.error('The hook script [' + path + '] does not extend res://addons/gut/hook_script.gd')
else:
result.valid = false
_lgr.error('The hook script [' + path + '] does not exist.')
return result
# ------------------------------------------------------------------------------
# Runs a hook script. Script must exist, and must extend
# res://addons/gut/hook_script.gd
# ------------------------------------------------------------------------------
func _run_hook_script(inst):
if(inst != null):
inst.gut = self
inst.run()
return inst
# ------------------------------------------------------------------------------
# Initialize variables for each run of a single test script.
# ------------------------------------------------------------------------------
func _init_run():
var valid = true
_test_collector.set_test_class_prefix(_inner_class_prefix)
_test_script_objects = []
_new_summary = _utils.Summary.new()
_log_text = ""
_current_test = null
_is_running = true
_yield_between.tests_since_last_yield = 0
_gui.get_text_box().clear_colors()
_gui.get_text_box().add_keyword_color("PASSED", Color(0, 1, 0))
_gui.get_text_box().add_keyword_color("FAILED", Color(1, 0, 0))
_gui.get_text_box().add_color_region('/#', '#/', Color(.9, .6, 0))
_gui.get_text_box().add_color_region('/-', '-/', Color(1, 1, 0))
_gui.get_text_box().add_color_region('/*', '*/', Color(.5, .5, 1))
var pre_hook_result = _validate_hook_script(_pre_run_script)
_pre_run_script_instance = pre_hook_result.instance
var post_hook_result = _validate_hook_script(_post_run_script)
_post_run_script_instance = post_hook_result.instance
valid = pre_hook_result.valid and post_hook_result.valid
return valid
# ------------------------------------------------------------------------------
# Print out run information and close out the run.
# ------------------------------------------------------------------------------
func _end_run():
var failed_tests = []
var more_than_one = _test_script_objects.size() > 1
p(_get_summary_text(), 0)
p("\n")
if(!_utils.is_null_or_empty(_select_script)):
p('Ran Scripts matching ' + _select_script)
if(!_utils.is_null_or_empty(_unit_test_name)):
p('Ran Tests matching ' + _unit_test_name)
if(!_utils.is_null_or_empty(_inner_class_name)):
p('Ran Inner Classes matching ' + _inner_class_name)
# For some reason the text edit control isn't scrolling to the bottom after
# the summary is printed. As a workaround, yield for a short time and
# then move the cursor. I found this workaround through trial and error.
_yield_between.timer.set_wait_time(0.1)
_yield_between.timer.start()
yield(_yield_between.timer, 'timeout')
_gui.get_text_box().cursor_set_line(_gui.get_text_box().get_line_count())
_is_running = false
update()
_run_hook_script(_post_run_script_instance)
emit_signal(SIGNAL_TESTS_FINISHED)
_gui.set_title("Finished. " + str(get_fail_count()) + " failures.")
# ------------------------------------------------------------------------------
# Checks the passed in thing to see if it is a "function state" object that gets
# returned when a function yields.
# ------------------------------------------------------------------------------
func _is_function_state(script_result):
return script_result != null and \
typeof(script_result) == TYPE_OBJECT and \
script_result is GDScriptFunctionState
# ------------------------------------------------------------------------------
# Print out the heading for a new script
# ------------------------------------------------------------------------------
func _print_script_heading(script):
if(_does_class_name_match(_inner_class_name, script.inner_class_name)):
p("\n/-----------------------------------------")
if(script.inner_class_name == null):
p("Running Script " + script.path, 0)
else:
p("Running Class [" + script.inner_class_name + "] in " + script.path, 0)
if(!_utils.is_null_or_empty(_inner_class_name) and _does_class_name_match(_inner_class_name, script.inner_class_name)):
p(str(' [',script.inner_class_name, '] matches [', _inner_class_name, ']'))
if(!_utils.is_null_or_empty(_unit_test_name)):
p(' Only running tests like: "' + _unit_test_name + '"')
p("-----------------------------------------/")
# ------------------------------------------------------------------------------
# Just gets more logic out of _test_the_scripts. Decides if we should yield after
# this test based on flags and counters.
# ------------------------------------------------------------------------------
func _should_yield_now():
var should = _yield_between.should and \
_yield_between.tests_since_last_yield == _yield_between.after_x_tests
if(should):
_yield_between.tests_since_last_yield = 0
else:
_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(the_class_name, script_class_name):
return (the_class_name == null or the_class_name == '') or (script_class_name != null and script_class_name.find(the_class_name) != -1)
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _setup_script(test_script):
test_script.gut = self
test_script.set_logger(_lgr)
add_child(test_script)
_test_script_objects.append(test_script)
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _do_yield_between(time):
_yield_between.timer.set_wait_time(time)
_yield_between.timer.start()
return _yield_between.timer
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _wait_for_done(result):
var iter_counter = 0
var print_after = 3
# sets waiting to false.
result.connect('completed', self, '_on_test_script_yield_completed')
if(!_was_yield_method_called):
p('/# Yield detected, waiting #/')
_was_yield_method_called = false
_waiting = true
_wait_timer.set_wait_time(0.25)
while(_waiting):
iter_counter += 1
if(iter_counter > print_after):
p(WAITING_MESSAGE, 2)
iter_counter = 0
_wait_timer.start()
yield(_wait_timer, 'timeout')
emit_signal('done_waiting')
# ------------------------------------------------------------------------------
# returns self so it can be integrated into the yield call.
# ------------------------------------------------------------------------------
func _wait_for_continue_button():
p(PAUSE_MESSAGE, 0)
_waiting = true
return self
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _call_deprecated_script_method(script, method, alt):
if(script.has_method(method)):
var txt = str(script, '-', method)
if(!_deprecated_tracker.has(txt)):
# Removing the deprecated line. I think it's still too early to
# start bothering people with this. Left everything here though
# because I don't want to remember how I did this last time.
#_lgr.deprecated(str('The method ', method, ' has been deprecated, use ', alt, ' instead.'))
_deprecated_tracker.add(txt)
script.call(method)
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _get_indexes_matching_script_name(name):
var indexes = [] # empty runs all
for i in range(_test_collector.scripts.size()):
if(_test_collector.scripts[i].get_filename().find(name) != -1):
indexes.append(i)
return indexes
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _get_indexes_matching_path(path):
var indexes = []
for i in range(_test_collector.scripts.size()):
if(_test_collector.scripts[i].path == path):
indexes.append(i)
return indexes
# ------------------------------------------------------------------------------
# Run all tests in a script. This is the core logic for running tests.
#
# Note, this has to stay as a giant monstrosity of a method because of the
# yields.
# ------------------------------------------------------------------------------
func _test_the_scripts(indexes=[]):
var is_valid = _init_run()
if(!is_valid):
_lgr.error('Something went wrong and the run was aborted.')
emit_signal(SIGNAL_TESTS_FINISHED)
return
_run_hook_script(_pre_run_script_instance)
if(_pre_run_script_instance!= null and _pre_run_script_instance.should_abort()):
_lgr.error('pre-run abort')
emit_signal(SIGNAL_TESTS_FINISHED)
return
_gui.run_mode()
var indexes_to_run = []
if(indexes.size()==0):
for i in range(_test_collector.scripts.size()):
indexes_to_run.append(i)
else:
indexes_to_run = indexes
_gui.set_progress_script_max(indexes_to_run.size()) # New way
_gui.set_progress_script_value(0)
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 test_indexes in range(indexes_to_run.size()):
var the_script = _test_collector.scripts[indexes_to_run[test_indexes]]
if(the_script.tests.size() > 0):
_gui.set_title('Running: ' + the_script.get_full_name())
_print_script_heading(the_script)
_new_summary.add_script(the_script.get_full_name())
var test_script = the_script.get_new()
var script_result = null
_setup_script(test_script)
_doubler.set_strategy(_double_strategy)
# yield between test scripts so things paint
if(_yield_between.should):
yield(_do_yield_between(0.01), 'timeout')
# !!!
# Hack so there isn't another indent to this monster of a method. if
# inner class is set and we do not have a match then empty the tests
# for the current test.
# !!!
if(!_does_class_name_match(_inner_class_name, the_script.inner_class_name)):
the_script.tests = []
else:
# call both pre-all-tests methods until prerun_setup is removed
_call_deprecated_script_method(test_script, 'prerun_setup', 'before_all')
test_script.before_all()
_gui.set_progress_test_max(the_script.tests.size()) # New way
# Each test in the script
for i in range(the_script.tests.size()):
_stubber.clear()
_spy.clear()
_doubler.clear_output_directory()
_current_test = the_script.tests[i]
if((_unit_test_name != '' and _current_test.name.find(_unit_test_name) > -1) or
(_unit_test_name == '')):
p(_current_test.name, 1)
_new_summary.add_test(_current_test.name)
# yield so things paint
if(_should_yield_now()):
yield(_do_yield_between(0.001), 'timeout')
_call_deprecated_script_method(test_script, 'setup', 'before_each')
test_script.before_each()
#When the script yields it will return a GDScriptFunctionState object
script_result = test_script.call(_current_test.name)
if(_is_function_state(script_result)):
_wait_for_done(script_result)
yield(self, 'done_waiting')
#if the test called pause_before_teardown then yield until
#the continue button is pressed.
if(_pause_before_teardown and !_ignore_pause_before_teardown):
_gui.pause()
yield(_wait_for_continue_button(), SIGNAL_STOP_YIELD_BEFORE_TEARDOWN)
test_script.clear_signal_watcher()
# call each post-each-test method until teardown is removed.
_call_deprecated_script_method(test_script, 'teardown', 'after_each')
test_script.after_each()
if(_current_test.passed):
_gui.get_text_box().add_keyword_color(_current_test.name, Color(0, 1, 0))
else:
_gui.get_text_box().add_keyword_color(_current_test.name, Color(1, 0, 0))
_gui.set_progress_test_value(i + 1)
_doubler.get_ignored_methods().clear()
# call both post-all-tests methods until postrun_teardown is removed.
if(_does_class_name_match(_inner_class_name, the_script.inner_class_name)):
_call_deprecated_script_method(test_script, 'postrun_teardown', 'after_all')
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
# into some other structure and kill the script objects with
# test_script.free() instead of remove child.
remove_child(test_script)
#END TESTS IN SCRIPT LOOP
_current_test = null
_gui.set_progress_script_value(test_indexes + 1) # new way
#END TEST SCRIPT LOOP
_end_run()
func _pass(text=''):
_gui.add_passing()
if(_current_test):
_new_summary.add_pass(_current_test.name, text)
func _fail(text=''):
_gui.add_failing()
if(_current_test != null):
var line_text = ' at line ' + str(_extractLineNumber( _current_test))
p(line_text, LOG_LEVEL_FAIL_ONLY)
# format for summary
line_text = "\n " + line_text
_new_summary.add_fail(_current_test.name, text + line_text)
_current_test.passed = false
# Extracts the line number from curren stacktrace by matching the test case name
func _extractLineNumber(current_test):
var line_number = current_test.line_number
# if stack trace available than extraxt the test case line number
var stackTrace = get_stack()
if(stackTrace!=null):
for index in stackTrace.size():
var line = stackTrace[index]
var function = line.get("function")
if function == current_test.name:
line_number = line.get("line")
return line_number
func _pending(text=''):
if(_current_test):
_new_summary.add_pending(_current_test.name, text)
# Gets all the files in a directory and all subdirectories if get_include_subdirectories
# is true. The files returned are all sorted by name.
func _get_files(path, prefix, suffix):
var files = []
var directories = []
var d = Directory.new()
d.open(path)
# true parameter tells list_dir_begin not to include "." and ".." directories.
d.list_dir_begin(true)
# 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
# returns an empty string. Then I guess you should end it.
var fs_item = d.get_next()
var full_path = ''
while(fs_item != ''):
full_path = path.plus_file(fs_item)
#file_exists returns fasle for directories
if(d.file_exists(full_path)):
if(fs_item.begins_with(prefix) and fs_item.ends_with(suffix)):
files.append(full_path)
elif(get_include_subdirectories() and d.dir_exists(full_path)):
directories.append(full_path)
fs_item = d.get_next()
d.list_dir_end()
for dir in range(directories.size()):
var dir_files = _get_files(directories[dir], prefix, suffix)
for i in range(dir_files.size()):
files.append(dir_files[i])
files.sort()
return files
#########################
#
# public
#
#########################
# ------------------------------------------------------------------------------
# Conditionally prints the text to the console/results variable based on the
# current log level and what level is passed in. Whenever currently in a test,
# the text will be indented under the test. It can be further indented if
# desired.
# ------------------------------------------------------------------------------
func p(text, level=0, indent=0):
var str_text = str(text)
var to_print = ""
var printing_test_name = false
if(level <= _utils.nvl(_log_level, 0)):
if(_current_test != null):
#make sure everything printed during the execution
#of a test is at least indented once under the test
if(indent == 0):
indent = 1
#Print the name of the current test if we haven't
#printed it already.
if(!_current_test.has_printed_name):
to_print = "* " + _current_test.name
_current_test.has_printed_name = true
printing_test_name = str_text == _current_test.name
if(!printing_test_name):
if(to_print != ""):
to_print += "\n"
#Make the indent
var pad = ""
for i in range(0, indent):
pad += " "
to_print += pad + str_text
to_print = to_print.replace("\n", "\n" + pad)
if(_should_print_to_console):
print(to_print)
_log_text += to_print + "\n"
_gui.get_text_box().insert_text_at_cursor(to_print + "\n")
################
#
# RUN TESTS/ADD SCRIPTS
#
################
func get_minimum_size():
return Vector2(810, 380)
# ------------------------------------------------------------------------------
# Runs all the scripts that were added using add_script
# ------------------------------------------------------------------------------
func test_scripts(run_rest=false):
clear_text()
if(_script_name != null and _script_name != ''):
var indexes = _get_indexes_matching_script_name(_script_name)
if(indexes == []):
_lgr.error('Could not find script matching ' + _script_name)
else:
_test_the_scripts(indexes)
else:
_test_the_scripts([])
# ------------------------------------------------------------------------------
# Runs a single script passed in.
# ------------------------------------------------------------------------------
func test_script(script):
_test_collector.set_test_class_prefix(_inner_class_prefix)
_test_collector.clear()
_test_collector.add_script(script)
_test_the_scripts()
# ------------------------------------------------------------------------------
# Adds a script to be run when test_scripts called
#
# No longer supports selecting a script via this method.
# ------------------------------------------------------------------------------
func add_script(script, was_select_this_one=null):
if(was_select_this_one != null):
_lgr.error('The option to select a script when using add_script has been removed. Calling add_script with 2 parameters will be removed in a later release.')
if(!Engine.is_editor_hint()):
_test_collector.set_test_class_prefix(_inner_class_prefix)
_test_collector.add_script(script)
_add_scripts_to_gui()
# ------------------------------------------------------------------------------
# Add all scripts in the specified directory that start with the prefix and end
# with the suffix. Does not look in sub directories. Can be called multiple
# times.
# ------------------------------------------------------------------------------
func add_directory(path, prefix=_file_prefix, suffix=_file_extension):
var d = Directory.new()
# check for '' b/c the calls to addin the exported directories 1-6 will pass
# '' if the field has not been populated. This will cause res:// to be
# processed which will include all files if include_subdirectories is true.
if(path == '' or !d.dir_exists(path)):
if(path != ''):
_lgr.error(str('The path [', path, '] does not exist.'))
return
var files = _get_files(path, prefix, suffix)
for i in range(files.size()):
add_script(files[i])
# ------------------------------------------------------------------------------
# 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 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
# ------------------------------------------------------------------------------
func select_script(script_name):
_script_name = script_name
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func export_tests(path=_export_path):
if(path == null):
_lgr.error('You must pass a path or set the export_path before calling export_tests')
else:
var result = _test_collector.export_tests(path)
if(result):
p(_test_collector.to_s())
p("Exported to " + path)
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func import_tests(path=_export_path):
if(!_utils.file_exists(path)):
_lgr.error(str('Cannot import tests: the path [', path, '] does not exist.'))
else:
_test_collector.clear()
var result = _test_collector.import_tests(path)
if(result):
p(_test_collector.to_s())
p("Imported from " + path)
_add_scripts_to_gui()
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func import_tests_if_none_found():
if(_test_collector.scripts.size() == 0):
import_tests()
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func export_if_tests_found():
if(_test_collector.scripts.size() > 0):
export_tests()
################
#
# MISC
#
################
# ------------------------------------------------------------------------------
# Maximize test runner window to fit the viewport.
# ------------------------------------------------------------------------------
func set_should_maximize(should):
_should_maximize = should
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_should_maximize():
return _should_maximize
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func maximize():
_gui.maximize()
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func disable_strict_datatype_checks(should):
_disable_strict_datatype_checks = should
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func is_strict_datatype_checks_disabled():
return _disable_strict_datatype_checks
# ------------------------------------------------------------------------------
# Pauses the test and waits for you to press a confirmation button. Useful when
# you want to watch a test play out onscreen or inspect results.
# ------------------------------------------------------------------------------
func end_yielded_test():
_lgr.deprecated('end_yielded_test is no longer necessary, you can remove it.')
# ------------------------------------------------------------------------------
# Clears the text of the text box. This resets all counters.
# ------------------------------------------------------------------------------
func clear_text():
_gui.get_text_box().set_text("")
_gui.get_text_box().clear_colors()
update()
# ------------------------------------------------------------------------------
# Get the number of tests that were ran
# ------------------------------------------------------------------------------
func get_test_count():
return _new_summary.get_totals().tests
# ------------------------------------------------------------------------------
# Get the number of assertions that were made
# ------------------------------------------------------------------------------
func get_assert_count():
var t = _new_summary.get_totals()
return t.passing + t.failing
# ------------------------------------------------------------------------------
# Get the number of assertions that passed
# ------------------------------------------------------------------------------
func get_pass_count():
return _new_summary.get_totals().passing
# ------------------------------------------------------------------------------
# Get the number of assertions that failed
# ------------------------------------------------------------------------------
func get_fail_count():
return _new_summary.get_totals().failing
# ------------------------------------------------------------------------------
# Get the number of tests flagged as pending
# ------------------------------------------------------------------------------
func get_pending_count():
return _new_summary.get_totals().pending
# ------------------------------------------------------------------------------
# Set whether it should print to console or not. Default is yes.
# ------------------------------------------------------------------------------
func set_should_print_to_console(should):
_should_print_to_console = should
# ------------------------------------------------------------------------------
# Get whether it is printing to the console
# ------------------------------------------------------------------------------
func get_should_print_to_console():
return _should_print_to_console
# ------------------------------------------------------------------------------
# Get the results of all tests ran as text. This string is the same as is
# displayed in the text box, and similar to what is printed to the console.
# ------------------------------------------------------------------------------
func get_result_text():
return _log_text
# ------------------------------------------------------------------------------
# Set the log level. Use one of the various LOG_LEVEL_* constants.
# ------------------------------------------------------------------------------
func set_log_level(level):
_log_level = level
if(!Engine.is_editor_hint()):
_gui.set_log_level(level)
# ------------------------------------------------------------------------------
# Get the current log level.
# ------------------------------------------------------------------------------
func get_log_level():
return _log_level
# ------------------------------------------------------------------------------
# Call this method to make the test pause before teardown so that you can inspect
# anything that you have rendered to the screen.
# ------------------------------------------------------------------------------
func pause_before_teardown():
_pause_before_teardown = true;
# ------------------------------------------------------------------------------
# For batch processing purposes, you may want to ignore any calls to
# pause_before_teardown that you forgot to remove.
# ------------------------------------------------------------------------------
func set_ignore_pause_before_teardown(should_ignore):
_ignore_pause_before_teardown = should_ignore
_gui.set_ignore_pause(should_ignore)
func get_ignore_pause_before_teardown():
return _ignore_pause_before_teardown
# ------------------------------------------------------------------------------
# Set to true so that painting of the screen will occur between tests. Allows you
# to see the output as tests occur. Especially useful with long running tests that
# make it appear as though it has humg.
#
# NOTE: not compatible with 1.0 so this is disabled by default. This will
# change in future releases.
# ------------------------------------------------------------------------------
func set_yield_between_tests(should):
_yield_between.should = should
func get_yield_between_tests():
return _yield_between.should
# ------------------------------------------------------------------------------
# Call _process or _fixed_process, if they exist, on obj and all it's children
# and their children and so and so forth. Delta will be passed through to all
# the _process or _fixed_process methods.
# ------------------------------------------------------------------------------
func simulate(obj, times, delta):
for i in range(times):
if(obj.has_method("_process")):
obj._process(delta)
if(obj.has_method("_physics_process")):
obj._physics_process(delta)
for kid in obj.get_children():
simulate(kid, 1, delta)
# ------------------------------------------------------------------------------
# Starts an internal timer with a timeout of the passed in time. A 'timeout'
# signal will be sent when the timer ends. Returns itself so that it can be
# used in a call to yield...cutting down on lines of code.
#
# Example, yield to the Gut object for 10 seconds:
# yield(gut.set_yield_time(10), 'timeout')
# ------------------------------------------------------------------------------
func set_yield_time(time, text=''):
_yield_timer.set_wait_time(time)
_yield_timer.start()
var msg = '/# Yielding (' + str(time) + 's)'
if(text == ''):
msg += ' #/'
else:
msg += ': ' + text + ' #/'
p(msg, 1)
_was_yield_method_called = true
return self
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func set_yield_signal_or_time(obj, signal_name, max_wait, text=''):
obj.connect(signal_name, self, '_yielding_callback', [true])
_yielding_to.obj = obj
_yielding_to.signal_name = signal_name
_yield_timer.set_wait_time(max_wait)
_yield_timer.start()
_was_yield_method_called = true
p(str('/# Yielding to signal "', signal_name, '" or for ', max_wait, ' seconds #/'))
return self
# ------------------------------------------------------------------------------
# get the specific unit test that should be run
# ------------------------------------------------------------------------------
func get_unit_test_name():
return _unit_test_name
# ------------------------------------------------------------------------------
# set the specific unit test that should be run.
# ------------------------------------------------------------------------------
func set_unit_test_name(test_name):
_unit_test_name = test_name
# ------------------------------------------------------------------------------
# Creates an empty file at the specified path
# ------------------------------------------------------------------------------
func file_touch(path):
var f = File.new()
f.open(path, f.WRITE)
f.close()
# ------------------------------------------------------------------------------
# deletes the file at the specified path
# ------------------------------------------------------------------------------
func file_delete(path):
var d = Directory.new()
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.
# ------------------------------------------------------------------------------
func is_file_empty(path):
var f = File.new()
f.open(path, f.READ)
var empty = f.get_len() == 0
f.close()
return empty
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_file_as_text(path):
var to_return = ''
var f = File.new()
f.open(path, f.READ)
to_return = f.get_as_text()
f.close()
return to_return
# ------------------------------------------------------------------------------
# deletes all files in a given directory
# ------------------------------------------------------------------------------
func directory_delete_files(path):
var d = Directory.new()
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
# returns an empty string. Then I guess you should end it.
d.list_dir_begin()
var thing = d.get_next() # could be a dir or a file or something else maybe?
var full_path = ''
while(thing != ''):
full_path = path + "/" + thing
#file_exists returns fasle for directories
if(d.file_exists(full_path)):
d.remove(full_path)
thing = d.get_next()
d.list_dir_end()
# ------------------------------------------------------------------------------
# Returns the instantiated script object that is currently being run.
# ------------------------------------------------------------------------------
func get_current_script_object():
var to_return = null
if(_test_script_objects.size() > 0):
to_return = _test_script_objects[-1]
return to_return
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_current_test_object():
return _current_test
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_stubber():
return _stubber
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_doubler():
return _doubler
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_spy():
return _spy
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_temp_directory():
return _temp_directory
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func set_temp_directory(temp_directory):
_temp_directory = temp_directory
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_inner_class_name():
return _inner_class_name
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func set_inner_class_name(inner_class_name):
_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):
_include_subdirectories = include_subdirectories
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_test_collector():
return _test_collector
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_export_path():
return _export_path
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func set_export_path(export_path):
_export_path = export_path
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_version():
return _version
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_pre_run_script():
return _pre_run_script
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func set_pre_run_script(pre_run_script):
_pre_run_script = pre_run_script
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_post_run_script():
return _post_run_script
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func set_post_run_script(post_run_script):
_post_run_script = post_run_script
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_pre_run_script_instance():
return _pre_run_script_instance
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func get_post_run_script_instance():
return _post_run_script_instance