diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index f8057221..492c4015 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -42,4 +42,4 @@ jobs: godot --path . --headless --import - name: Run tests run: | - godot --path . --headless --script addons/gut/gut_cmdln.gd -gexit + godot --path . --headless -s addons/gut/gut_cmdln.gd -gexit diff --git a/addons/block_code/README.md b/addons/block_code/README.md index 5d328813..5d871e87 100644 --- a/addons/block_code/README.md +++ b/addons/block_code/README.md @@ -153,14 +153,19 @@ This plugin uses the [Godot Unit Test](https://gut.readthedocs.io/en/latest/) (G Tests can also be run from the command line using the GUT command line script: ``` -godot --path . --headless --script addons/gut/gut_cmdln.gd -gexit +godot --path . --headless -s addons/gut/gut_cmdln.gd -gexit ``` -A few options are of note here. `--path` instructs Godot to use the project in -the current directory. `--headless` instructs Godot to run without a display or -sound. `--script` instructs Godot to run the GUT command line script instead of -running the main scene. `-gexit` is an option for the GUT command line script -that instructs GUT to exit after the tests complete. +A few options are of note here: + +- `--path` instructs Godot to use the project in the current directory. +- `--headless` instructs Godot to run without a display or sound. +- `-s` instructs Godot to run the GUT command line script instead of + running the main scene. Due to a [bug in + GUT](https://github.com/bitwes/Gut/issues/667), the long form `--script` + cannot be used. +- `-gexit` is an option for the GUT command line script that instructs GUT to + exit after the tests complete. There are several other GUT command line options for running specific tests. For example, `-gtest=path/to/test_script_1.gd,path/to/test_script_2.gd` can be diff --git a/addons/gut/GutScene.gd b/addons/gut/GutScene.gd index d9362ab2..c38b1d34 100644 --- a/addons/gut/GutScene.gd +++ b/addons/gut/GutScene.gd @@ -125,3 +125,6 @@ func use_compact_mode(should=true): func set_opacity(val): _normal_gui.modulate.a = val _compact_gui.modulate.a = val + +func set_title(text): + _set_both_titles(text) diff --git a/addons/gut/autofree.gd b/addons/gut/autofree.gd index b82676bc..e35f7f57 100644 --- a/addons/gut/autofree.gd +++ b/addons/gut/autofree.gd @@ -55,5 +55,3 @@ func free_all(): if(is_instance_valid(_to_queue_free[i])): _to_queue_free[i].queue_free() _to_queue_free.clear() - - diff --git a/addons/gut/awaiter.gd b/addons/gut/awaiter.gd index 773850a0..c36f59ea 100644 --- a/addons/gut/awaiter.gd +++ b/addons/gut/awaiter.gd @@ -3,12 +3,21 @@ extends Node signal timeout signal wait_started -var _wait_time = 0.0 -var _wait_frames = 0 +var _wait_time := 0.0 +var _wait_frames := 0 var _signal_to_wait_on = null -var _elapsed_time = 0.0 -var _elapsed_frames = 0 +var _predicate_function_waiting_to_be_true = null +var _predicate_time_between := 0.0 +var _predicate_time_between_elpased := 0.0 + +var _did_last_wait_timeout = false +var did_last_wait_timeout = false : + get: return _did_last_wait_timeout + set(val): push_error("Cannot set did_last_wait_timeout") + +var _elapsed_time := 0.0 +var _elapsed_frames := 0 func _physics_process(delta): @@ -22,14 +31,30 @@ func _physics_process(delta): if(_elapsed_frames >= _wait_frames): _end_wait() + if(_predicate_function_waiting_to_be_true != null): + _predicate_time_between_elpased += delta + if(_predicate_time_between_elpased >= _predicate_time_between): + _predicate_time_between_elpased = 0.0 + var result = _predicate_function_waiting_to_be_true.call() + if(typeof(result) == TYPE_BOOL and result): + _end_wait() + func _end_wait(): + # Check for time before checking for frames so that the extra frames added + # when waiting on a signal do not cause a false negative for timing out. + if(_wait_time > 0): + _did_last_wait_timeout = _elapsed_time >= _wait_time + elif(_wait_frames > 0): + _did_last_wait_timeout = _elapsed_frames >= _wait_frames + if(_signal_to_wait_on != null and _signal_to_wait_on.is_connected(_signal_callback)): _signal_to_wait_on.disconnect(_signal_callback) _wait_time = 0.0 _wait_frames = 0 _signal_to_wait_on = null + _predicate_function_waiting_to_be_true = null _elapsed_time = 0.0 _elapsed_frames = 0 timeout.emit() @@ -37,9 +62,9 @@ func _end_wait(): const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' func _signal_callback( - arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, - arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, - arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET): + _arg1=ARG_NOT_SET, _arg2=ARG_NOT_SET, _arg3=ARG_NOT_SET, + _arg4=ARG_NOT_SET, _arg5=ARG_NOT_SET, _arg6=ARG_NOT_SET, + _arg7=ARG_NOT_SET, _arg8=ARG_NOT_SET, _arg9=ARG_NOT_SET): _signal_to_wait_on.disconnect(_signal_callback) # DO NOT _end_wait here. For other parts of the test to get the signal that @@ -47,24 +72,34 @@ func _signal_callback( # signal_watcher doesn't get the signal in time if we don't do this. _wait_frames = 2 - -func wait_for(x): +func wait_seconds(x): + _did_last_wait_timeout = false _wait_time = x wait_started.emit() func wait_frames(x): + _did_last_wait_timeout = false _wait_frames = x wait_started.emit() -func wait_for_signal(the_signal, x): +func wait_for_signal(the_signal, max_time): + _did_last_wait_timeout = false the_signal.connect(_signal_callback) _signal_to_wait_on = the_signal - _wait_time = x + _wait_time = max_time + wait_started.emit() + + +func wait_until(predicate_function: Callable, max_time, time_between_calls:=0.0): + _predicate_time_between = time_between_calls + _predicate_function_waiting_to_be_true = predicate_function + _predicate_time_between_elpased = 0.0 + _did_last_wait_timeout = false + _wait_time = max_time wait_started.emit() func is_waiting(): return _wait_time != 0.0 || _wait_frames != 0 - diff --git a/addons/gut/cli/gut_cli.gd b/addons/gut/cli/gut_cli.gd new file mode 100644 index 00000000..df5613f6 --- /dev/null +++ b/addons/gut/cli/gut_cli.gd @@ -0,0 +1,298 @@ +extends Node + +var Optparse = load('res://addons/gut/cli/optparse.gd') +var Gut = load('res://addons/gut/gut.gd') +var GutRunner = load('res://addons/gut/gui/GutRunner.tscn') + +# ------------------------------------------------------------------------------ +# Helper class to resolve the various different places where an option can +# be set. Using the get_value method will enforce the order of precedence of: +# 1. command line value +# 2. config file value +# 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 precedented hashes +# will punch through null values of higher precedented hashes. +# ------------------------------------------------------------------------------ +class OptionResolver: + var base_opts = {} + var cmd_opts = {} + var config_opts = {} + + + func get_value(key): + return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key])) + + func set_base_opts(opts): + base_opts = opts + cmd_opts = _null_copy(opts) + config_opts = _null_copy(opts) + + # creates a copy of a hash with all values null. + func _null_copy(h): + var new_hash = {} + for key in h: + new_hash[key] = null + return new_hash + + func _nvl(a, b): + if(a == null): + return b + else: + return a + + func _string_it(h): + var to_return = '' + for key in h: + to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')') + return to_return + + func to_s(): + return str("base:\n", _string_it(base_opts), "\n", \ + "config:\n", _string_it(config_opts), "\n", \ + "cmd:\n", _string_it(cmd_opts), "\n", \ + "resolved:\n", _string_it(get_resolved_values())) + + func get_resolved_values(): + var to_return = {} + for key in base_opts: + to_return[key] = get_value(key) + return to_return + + func to_s_verbose(): + var to_return = '' + var resolved = get_resolved_values() + for key in base_opts: + to_return += str(key, "\n") + to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n") + to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n") + to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n") + to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n") + + return to_return + +# ------------------------------------------------------------------------------ +# Here starts the actual script that uses the Options class to kick off Gut +# and run your tests. +# ------------------------------------------------------------------------------ +var _gut_config = load('res://addons/gut/gut_config.gd').new() + +# array of command line options specified +var _final_opts = [] + + +func setup_options(options, font_names): + var opts = Optparse.new() + opts.banner =\ +""" +The GUT CLI +----------- +The default behavior for GUT is to load options from a res://.gutconfig.json if +it exists. Any options specified on the command line will take precedence over +options specified in the gutconfig file. You can specify a different gutconfig +file with the -gconfig option. + +To generate a .gutconfig.json file you can use -gprint_gutconfig_sample +To see the effective values of a CLI command and a gutconfig use -gpo + +Values for options can be supplied using: + option=value # no space around "=" + option value # a space between option and value w/o = + +Options whose values are lists/arrays can be specified multiple times: + -gdir=a,b + -gdir c,d + -gdir e + # results in -gdir equaling [a, b, c, d, e] +""" + opts.add_heading("Test Config:") + opts.add('-gdir', options.dirs, 'List of directories to search for test scripts in.') + opts.add('-ginclude_subdirs', false, 'Flag to include all subdirectories specified with -gdir.') + opts.add('-gtest', [], 'List of full paths to test scripts to run.') + opts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir. Default "[default]".') + opts.add('-gsuffix', options.suffix, 'Test script suffix, including .gd extension. Default "[default]".') + opts.add('-gconfig', 'res://.gutconfig.json', 'The config file to load options from. The default is [default]. Use "-gconfig=" to not use a config file.') + opts.add('-gpre_run_script', '', 'pre-run hook script path') + opts.add('-gpost_run_script', '', 'post-run hook script path') + opts.add('-gerrors_do_not_cause_failure', false, 'When an internal GUT error occurs tests will fail. With this option set, that does not happen.') + opts.add('-gdouble_strategy', 'SCRIPT_ONLY', 'Default strategy to use when doubling. Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY]. Default "[default]"') + + opts.add_heading("Run Options:") + opts.add('-gselect', '', 'All scripts that contain the specified string in their filename will be ran') + opts.add('-ginner_class', '', 'Only run inner classes that contain the specified string in their name.') + opts.add('-gunit_test_name', '', 'Any test that contains the specified text will be run, all others will be skipped.') + opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.') + opts.add('-gexit_on_success', false, 'Only exit if zero tests fail.') + opts.add('-gignore_pause', false, 'Ignores any calls to pause_before_teardown.') + + opts.add_heading("Display Settings:") + opts.add('-glog', options.log_level, 'Log level [0-3]. Default [default]') + opts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts. Default [default].') + opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.') + opts.add('-gcompact_mode', false, 'The runner will be in compact mode. This overrides -gmaximize.') + opts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.') + opts.add('-gdisable_colors', false, 'Disable command line colors.') + opts.add('-gfont_name', options.font_name, str('Valid values are: ', font_names, '. Default "[default]"')) + opts.add('-gfont_size', options.font_size, 'Font size, default "[default]"') + opts.add('-gbackground_color', options.background_color, 'Background color as an html color, default "[default]"') + opts.add('-gfont_color',options.font_color, 'Font color as an html color, default "[default]"') + opts.add('-gpaint_after', options.paint_after, 'Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]') + + opts.add_heading("Result Export:") + opts.add('-gjunit_xml_file', options.junit_xml_file, 'Export results of run to this file in the Junit XML format.') + opts.add('-gjunit_xml_timestamp', options.junit_xml_timestamp, 'Include a timestamp in the -gjunit_xml_file, default [default]') + + opts.add_heading("Help:") + opts.add('-gh', false, 'Print this help. You did this to see this, so you probably understand.') + opts.add('-gpo', false, 'Print option values from all sources and the value used.') + opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file.') + + return opts + + +# Parses options, applying them to the _tester or setting values +# in the options struct. +func extract_command_line_options(from, to): + to.config_file = from.get_value_or_null('-gconfig') + to.dirs = from.get_value_or_null('-gdir') + to.disable_colors = from.get_value_or_null('-gdisable_colors') + to.double_strategy = from.get_value_or_null('-gdouble_strategy') + to.ignore_pause = from.get_value_or_null('-gignore_pause') + to.include_subdirs = from.get_value_or_null('-ginclude_subdirs') + to.inner_class = from.get_value_or_null('-ginner_class') + to.log_level = from.get_value_or_null('-glog') + to.opacity = from.get_value_or_null('-gopacity') + to.post_run_script = from.get_value_or_null('-gpost_run_script') + to.pre_run_script = from.get_value_or_null('-gpre_run_script') + to.prefix = from.get_value_or_null('-gprefix') + to.selected = from.get_value_or_null('-gselect') + to.should_exit = from.get_value_or_null('-gexit') + to.should_exit_on_success = from.get_value_or_null('-gexit_on_success') + to.should_maximize = from.get_value_or_null('-gmaximize') + to.compact_mode = from.get_value_or_null('-gcompact_mode') + to.hide_orphans = from.get_value_or_null('-ghide_orphans') + to.suffix = from.get_value_or_null('-gsuffix') + to.errors_do_not_cause_failure = from.get_value_or_null('-gerrors_do_not_cause_failure') + to.tests = from.get_value_or_null('-gtest') + to.unit_test_name = from.get_value_or_null('-gunit_test_name') + + to.font_size = from.get_value_or_null('-gfont_size') + to.font_name = from.get_value_or_null('-gfont_name') + to.background_color = from.get_value_or_null('-gbackground_color') + to.font_color = from.get_value_or_null('-gfont_color') + to.paint_after = from.get_value_or_null('-gpaint_after') + + to.junit_xml_file = from.get_value_or_null('-gjunit_xml_file') + to.junit_xml_timestamp = from.get_value_or_null('-gjunit_xml_timestamp') + + + +func _print_gutconfigs(values): + var header = """Here is a sample of a full .gutconfig.json file. +You do not need to specify all values in your own file. The values supplied in +this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample +option. Option priority is: command-line, .gutconfig, default).""" + print("\n", header.replace("\n", ' '), "\n") + var resolved = values + + # remove_at some options that don't make sense to be in config + resolved.erase("config_file") + resolved.erase("show_help") + + print(JSON.stringify(resolved, ' ')) + + for key in resolved: + resolved[key] = null + + print("\n\nAnd here's an empty config for you fill in what you want.") + print(JSON.stringify(resolved, ' ')) + + +func _run_tests(opt_resolver): + _final_opts = opt_resolver.get_resolved_values(); + _gut_config.options = _final_opts + + var runner = GutRunner.instantiate() + runner.ran_from_editor = false + runner.set_gut_config(_gut_config) + get_tree().root.add_child(runner) + + runner.run_tests() + + +# parse options and run Gut +func main(): + var opt_resolver = OptionResolver.new() + opt_resolver.set_base_opts(_gut_config.default_options) + + var cli_opts = setup_options(_gut_config.default_options, _gut_config.valid_fonts) + + cli_opts.parse() + var all_options_valid = cli_opts.unused.size() == 0 + extract_command_line_options(cli_opts, opt_resolver.cmd_opts) + + var config_path = opt_resolver.get_value('config_file') + var load_result = 1 + # Checking for an empty config path allows us to not use a config file via + # the -gconfig_file option since using "-gconfig_file=" or -gconfig_file=''" + # will result in an empty string. + if(config_path != ''): + load_result = _gut_config.load_options_no_defaults(config_path) + + # SHORTCIRCUIT + if(!all_options_valid): + print('Unknown arguments: ', cli_opts.unused) + get_tree().quit(1) + elif(load_result == -1): + print('Invalid gutconfig ', load_result) + get_tree().quit(1) + else: + opt_resolver.config_opts = _gut_config.options + + if(cli_opts.get_value('-gh')): + print(GutUtils.version_numbers.get_version_text()) + cli_opts.print_help() + get_tree().quit(0) + elif(cli_opts.get_value('-gpo')): + print('All config options and where they are specified. ' + + 'The "final" value shows which value will actually be used ' + + 'based on order of precedence (default < .gutconfig < cmd line).' + "\n") + print(opt_resolver.to_s_verbose()) + get_tree().quit(0) + elif(cli_opts.get_value('-gprint_gutconfig_sample')): + _print_gutconfigs(opt_resolver.get_resolved_values()) + get_tree().quit(0) + else: + _run_tests(opt_resolver) + + + +# ############################################################################## +#(G)odot (U)nit (T)est class +# +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2023 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. +# +# ############################################################################## diff --git a/addons/gut/cli/optparse.gd b/addons/gut/cli/optparse.gd new file mode 100644 index 00000000..3c6433f6 --- /dev/null +++ b/addons/gut/cli/optparse.gd @@ -0,0 +1,511 @@ +# ############################################################################## +# Parses options from the command line, as one might expect. It can also +# generate help text that displays all the arguments your script accepts. +# +# This does alot, if you want to see it in action have a look at +# scratch/optparse_example.gd +# +# +# Godot Argument Lists +# ------------------------- +# There are two sets of command line arguments that Godot populates: +# OS.get_cmdline_args +# OS.get_cmdline_user_args. +# +# OS.get_cmdline_args contains any arguments that are not used by the engine +# itself. This means options like --help and -d will never appear in this list +# since these are used by the engine. The one exception is the -s option which +# is always included as the first entry and the script path as the second. +# Optparse ignores these values for argument processing but can be accessed +# with my_optparse.options.script_option. This list does not contain any +# arguments that appear in OS.get_cmdline_user_args. +# +# OS.get_cmdline_user_args contains any arguments that appear on the command +# line AFTER " -- " or " ++ ". This list CAN contain options that the engine +# would otherwise use, and are ignored completely by the engine. +# +# The parse method, by default, includes arguments from OS.get_cmdline_args and +# OS.get_cmdline_user_args. You can optionally pass one of these to the parse +# method to limit which arguments are parsed. You can also conjure up your own +# array of arguments and pass that to parse. +# +# See Godot's documentation for get_cmdline_args and get_cmdline_user_args for +# more information. +# +# +# Adding Options +# -------------- +# Use the following to add options to be parsed. These methods return the +# created Option instance. See that class above for more info. You can use +# the returned instance to get values, or use get_value/get_value_or_null. +# add("--name", "default", "Description goes here") +# add_required("--name", "default", "Description goes here") +# add_positional("--name", "default", "Description goes here") +# add_positional_required("--name", "default", "Description goes here") +# +# get_value will return the value of the option or the default if it was not +# set. get_value_or_null will return the value of the option or null if it was +# not set. +# +# The Datatype for an option is determined from the default value supplied to +# the various add methods. Supported types are +# String +# Int +# Float +# Array of strings +# Boolean +# +# +# Value Parsing +# ------------- +# optparse uses option_name_prefix to differentiate between option names and +# values. Any argument that starts with this value will be treated as an +# argument name. The default is "-". Set this before calling parse if you want +# to change it. +# +# Values for options can be supplied on the command line with or without an "=": +# option=value # no space around "=" +# option value # a space between option and value w/o = +# There is no way to escape "=" at this time. +# +# Array options can be specified multiple times and/or set from a comma delimited +# list. +# -gdir=a,b +# -gdir c,d +# -gdir e +# Results in -gdir equaling [a, b, c, d, e]. There is no way to escape commas +# at this time. +# +# To specify an empty list via the command line follow the option with an equal +# sign +# -gdir= +# +# Boolean options will have thier value set to !default when they are supplied +# on the command line. Boolean options cannot have a value on the command line. +# They are either supplied or not. +# +# If a value is not an array and is specified multiple times on the command line +# then the last entry will be used as the value. +# +# Positional argument values are parsed after all named arguments are parsed. +# This means that other options can appear before, between, and after positional +# arguments. +# --foo=bar positional_0_value --disabled --bar foo positional_1_value --a_flag +# +# Anything that is not used by named or positional arguments will appear in the +# unused property. You can use this to detect unrecognized arguments or treat +# everything else provided as a list of things, or whatever you want. You can +# use is_option on the elements of unused (or whatever you want really) to see +# if optparse would treat it as an option name. +# +# Use get_missing_required_options to get an array of Option with all required +# options that were not found when parsing. +# +# The parsed_args property holds the list of arguments that were parsed. +# +# +# Help Generation +# --------------- +# You can call get_help to generate help text, or you can just call print_help +# and this will print it for you. +# +# Set the banner property to any text you want to appear before the usage and +# options sections. +# +# Options are printed in the order they are added. You can add a heading for +# different options sections with add_heading. +# add("--asdf", 1, "This will have no heading") +# add_heading("foo") +# add("--foo", false, "This will have the foo heading") +# add("--another_foo", 1.5, "This too.") +# add_heading("This is after foo") +# add("--bar", true, "You probably get it by now.") +# +# If you include "[default]" in the description of a option, then the help will +# substitue it with the default value. +# +# ############################################################################## + + +#------------------------------------------------------------------------------- +# Holds all the properties of a command line option +# +# value will return the default when it has not been set. +#------------------------------------------------------------------------------- +class Option: + var _has_been_set = false + var _value = null + # REMEMBER that when this option is an array, you have to set the value + # before you alter the contents of the array (append etc) or has_been_set + # will return false and it might not be used right. For example + # get_value_or_null will return null when you've actually changed the value. + var value = _value: + get: + return _value + + set(val): + _has_been_set = true + _value = val + + var option_name = '' + var default = null + var description = '' + var required = false + + + func _init(name,default_value,desc=''): + option_name = name + default = default_value + description = desc + _value = default + + + func to_s(min_space=0): + var subbed_desc = description + subbed_desc = subbed_desc.replace('[default]', str(default)) + return str(option_name.rpad(min_space), ' ', subbed_desc) + + + func has_been_set(): + return _has_been_set + + + + +#------------------------------------------------------------------------------- +# A struct for organizing options by a heading +#------------------------------------------------------------------------------- +class OptionHeading: + var options = [] + var display = 'default' + + + + +#------------------------------------------------------------------------------- +# Organizes options by order, heading, position. Also responsible for all +# help related text generation. +#------------------------------------------------------------------------------- +class Options: + var options = [] + var positional = [] + var default_heading = OptionHeading.new() + var script_option = Option.new('-s', '?', 'script option provided by Godot') + + var _options_by_name = {} + var _options_by_heading = [default_heading] + var _cur_heading = default_heading + + + func add_heading(display): + var heading = OptionHeading.new() + heading.display = display + _cur_heading = heading + _options_by_heading.append(heading) + + + func add(option): + options.append(option) + _options_by_name[option.option_name] = option + _cur_heading.options.append(option) + + + func add_positional(option): + positional.append(option) + _options_by_name[option.option_name] = option + + + func get_by_name(option_name): + var found_param = null + if(option_name == script_option.option_name): + found_param = script_option + elif(_options_by_name.has(option_name)): + found_param = _options_by_name[option_name] + + return found_param + + + func get_help_text(): + var longest = 0 + var text = "" + for i in range(options.size()): + if(options[i].option_name.length() > longest): + longest = options[i].option_name.length() + + for heading in _options_by_heading: + if(heading != default_heading): + text += str("\n", heading.display, "\n") + for option in heading.options: + text += str(' ', option.to_s(longest + 2), "\n") + + + return text + + + func get_option_value_text(): + var text = "" + var i = 0 + for option in positional: + text += str(i, '. ', option.option_name, ' = ', option.value) + + if(!option.has_been_set()): + text += " (default)" + text += "\n" + i += 1 + + for option in options: + text += str(option.option_name, ' = ', option.value) + + if(!option.has_been_set()): + text += " (default)" + text += "\n" + return text + + + func print_option_values(): + print(get_option_value_text()) + + + func get_missing_required_options(): + var to_return = [] + for opt in options: + if(opt.required and !opt.has_been_set()): + to_return.append(opt) + + for opt in positional: + if(opt.required and !opt.has_been_set()): + to_return.append(opt) + + return to_return + + + func get_usage_text(): + var pos_text = "" + for opt in positional: + pos_text += str("[", opt.description, "] ") + + if(pos_text != ""): + pos_text += " [opts] " + + return " -s " + script_option.value + " [opts] " + pos_text + + + + +#------------------------------------------------------------------------------- +# +# optarse +# +#------------------------------------------------------------------------------- +var options = Options.new() +var banner = '' +var option_name_prefix = '-' +var unused = [] +var parsed_args = [] + +func _convert_value_to_array(raw_value): + var split = raw_value.split(',') + # This is what an empty set looks like from the command line. If we do + # not do this then we will always get back [''] which is not what it + # shoudl be. + if(split.size() == 1 and split[0] == ''): + split = [] + return split + + +# REMEMBER raw_value not used for bools. +func _set_option_value(option, raw_value): + var t = typeof(option.default) + # only set values that were specified at the command line so that + # we can punch through default and config values correctly later. + # Without this check, you can't tell the difference between the + # defaults and what was specified, so you can't punch through + # higher level options. + if(t == TYPE_INT): + option.value = int(raw_value) + elif(t == TYPE_STRING): + option.value = str(raw_value) + elif(t == TYPE_ARRAY): + var values = _convert_value_to_array(raw_value) + if(!option.has_been_set()): + option.value = [] + option.value.append_array(values) + elif(t == TYPE_BOOL): + option.value = !option.default + elif(t == TYPE_FLOAT): + option.value = float(raw_value) + elif(t == TYPE_NIL): + print(option.option_name + ' cannot be processed, it has a nil datatype') + else: + print(option.option_name + ' cannot be processed, it has unknown datatype:' + str(t)) + + +func _parse_command_line_arguments(args): + var parsed_opts = args.duplicate() + var i = 0 + var positional_index = 0 + + while i < parsed_opts.size(): + var opt = '' + var value = '' + var entry = parsed_opts[i] + + if(is_option(entry)): + if(entry.find('=') != -1): + var parts = entry.split('=') + opt = parts[0] + value = parts[1] + var the_option = options.get_by_name(opt) + if(the_option != null): + parsed_opts.remove_at(i) + _set_option_value(the_option, value) + else: + i += 1 + else: + var the_option = options.get_by_name(entry) + if(the_option != null): + parsed_opts.remove_at(i) + if(typeof(the_option.default) == TYPE_BOOL): + _set_option_value(the_option, null) + elif(i < parsed_opts.size() and !is_option(parsed_opts[i])): + value = parsed_opts[i] + parsed_opts.remove_at(i) + _set_option_value(the_option, value) + else: + i += 1 + else: + if(positional_index < options.positional.size()): + _set_option_value(options.positional[positional_index], entry) + parsed_opts.remove_at(i) + positional_index += 1 + else: + i += 1 + + # this is the leftovers that were not extracted. + return parsed_opts + + +func is_option(arg): + return arg.begins_with(option_name_prefix) + + +func add(op_name, default, desc): + var new_op = null + + if(options.get_by_name(op_name) != null): + push_error(str('Option [', op_name, '] already exists.')) + else: + new_op = Option.new(op_name, default, desc) + options.add(new_op) + + return new_op + + +func add_required(op_name, default, desc): + var op = add(op_name, default, desc) + if(op != null): + op.required = true + return op + + +func add_positional(op_name, default, desc): + var new_op = null + if(options.get_by_name(op_name) != null): + push_error(str('Positional option [', op_name, '] already exists.')) + else: + new_op = Option.new(op_name, default, desc) + options.add_positional(new_op) + return new_op + + +func add_positional_required(op_name, default, desc): + var op = add_positional(op_name, default, desc) + if(op != null): + op.required = true + return op + + +func add_heading(display_text): + options.add_heading(display_text) + + +func get_value(name): + var found_param = options.get_by_name(name) + + if(found_param != null): + return found_param.value + else: + print("COULD NOT FIND OPTION " + name) + return null + + +# This will return null instead of the default value if an option has not been +# specified. This can be useful when providing an order of precedence to your +# values. For example if +# default value < config file < command line +# then you do not want to get the default value for a command line option or it +# will overwrite the value in a config file. +func get_value_or_null(name): + var found_param = options.get_by_name(name) + + if(found_param != null and found_param.has_been_set()): + return found_param.value + else: + return null + + +func get_help(): + var sep = '---------------------------------------------------------' + + var text = str(sep, "\n", banner, "\n\n") + text += "Usage\n-----------\n" + text += " " + options.get_usage_text() + "\n\n" + text += "\nOptions\n-----------\n" + text += options.get_help_text() + text += str(sep, "\n") + return text + + +func print_help(): + print(get_help()) + + +func parse(cli_args=null): + parsed_args = cli_args + + if(parsed_args == null): + parsed_args = OS.get_cmdline_args() + parsed_args.append_array(OS.get_cmdline_user_args()) + + unused = _parse_command_line_arguments(parsed_args) + + +func get_missing_required_options(): + return options.get_missing_required_options() + + +# ############################################################################## +# The MIT License (MIT) +# ===================== +# +# Copyright (c) 2024 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. +# +# ############################################################################## \ No newline at end of file diff --git a/addons/gut/collected_script.gd b/addons/gut/collected_script.gd index 6f8f5eba..4542da9c 100644 --- a/addons/gut/collected_script.gd +++ b/addons/gut/collected_script.gd @@ -6,9 +6,8 @@ # # This class also facilitates all the exporting and importing of tests. # ------------------------------------------------------------------------------ -var CollectedTest = load('res://addons/gut/collected_test.gd') +var CollectedTest = GutUtils.CollectedTest -var _utils = null var _lgr = null # One entry per test found in the script. Added externally by TestCollector @@ -34,180 +33,174 @@ var was_run = false var name = '' : - get: return path - set(val):pass + get: return path + set(val):pass -func _init(utils=null,logger=null): - _utils = utils - _lgr = logger +func _init(logger=null): + _lgr = logger func get_new(): - return load_script().new() + return load_script().new() func load_script(): - var to_return = load(path) + var to_return = load(path) - if(inner_class_name != null and inner_class_name != ''): - # If we wanted to do inner classes in inner classses - # then this would have to become some kind of loop or recursive - # call to go all the way down the chain or this class would - # have to change to hold onto the loaded class instead of - # just path information. - to_return = to_return.get(inner_class_name) + if(inner_class_name != null and inner_class_name != ''): + # If we wanted to do inner classes in inner classses + # then this would have to become some kind of loop or recursive + # call to go all the way down the chain or this class would + # have to change to hold onto the loaded class instead of + # just path information. + to_return = to_return.get(inner_class_name) - return to_return + return to_return # script.gd.InnerClass func get_filename_and_inner(): - var to_return = get_filename() - if(inner_class_name != ''): - to_return += '.' + String(inner_class_name) - return to_return + var to_return = get_filename() + if(inner_class_name != ''): + to_return += '.' + String(inner_class_name) + return to_return # res://foo/bar.gd.FooBar func get_full_name(): - var to_return = path - if(inner_class_name != ''): - to_return += '.' + String(inner_class_name) - return to_return + var to_return = path + if(inner_class_name != ''): + to_return += '.' + String(inner_class_name) + return to_return func get_filename(): - return path.get_file() + return path.get_file() func has_inner_class(): - return inner_class_name != '' + return inner_class_name != '' # Note: although this no longer needs to export the inner_class names since # they are pulled from metadata now, it is easier to leave that in # so we don't have to cut the export down to unique script names. func export_to(config_file, section): - config_file.set_value(section, 'path', path) - config_file.set_value(section, 'inner_class', inner_class_name) - var names = [] - for i in range(tests.size()): - names.append(tests[i].name) - config_file.set_value(section, 'tests', names) + config_file.set_value(section, 'path', path) + config_file.set_value(section, 'inner_class', inner_class_name) + var names = [] + for i in range(tests.size()): + names.append(tests[i].name) + config_file.set_value(section, 'tests', names) func _remap_path(source_path): - var to_return = source_path - if(!_utils.file_exists(source_path)): - _lgr.debug('Checking for remap for: ' + source_path) - var remap_path = source_path.get_basename() + '.gd.remap' - if(_utils.file_exists(remap_path)): - var cf = ConfigFile.new() - cf.load(remap_path) - to_return = cf.get_value('remap', 'path') - else: - _lgr.warn('Could not find remap file ' + remap_path) - return to_return + var to_return = source_path + if(!FileAccess.file_exists(source_path)): + _lgr.debug('Checking for remap for: ' + source_path) + var remap_path = source_path.get_basename() + '.gd.remap' + if(FileAccess.file_exists(remap_path)): + var cf = ConfigFile.new() + cf.load(remap_path) + to_return = cf.get_value('remap', 'path') + else: + _lgr.warn('Could not find remap file ' + remap_path) + return to_return func import_from(config_file, section): - path = config_file.get_value(section, 'path') - path = _remap_path(path) - # Null is an acceptable value, but you can't pass null as a default to - # get_value since it thinks you didn't send a default...then it spits - # out red text. This works around that. - var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder') - if(inner_name != 'Placeholder'): - inner_class_name = inner_name - else: # just being explicit - inner_class_name = StringName("") + path = config_file.get_value(section, 'path') + path = _remap_path(path) + # Null is an acceptable value, but you can't pass null as a default to + # get_value since it thinks you didn't send a default...then it spits + # out red text. This works around that. + var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder') + if(inner_name != 'Placeholder'): + inner_class_name = inner_name + else: # just being explicit + inner_class_name = StringName("") -func get_test_named(name): - return _utils.search_array(tests, 'name', name) - - -func mark_tests_to_skip_with_suffix(suffix): - for single_test in tests: - single_test.should_skip = single_test.name.ends_with(suffix) +func get_test_named(test_name): + return GutUtils.search_array(tests, 'name', test_name) func get_ran_test_count(): - var count = 0 - for t in tests: - if(t.was_run): - count += 1 - return count + var count = 0 + for t in tests: + if(t.was_run): + count += 1 + return count func get_assert_count(): - var count = 0 - for t in tests: - count += t.pass_texts.size() - count += t.fail_texts.size() - for t in setup_teardown_tests: - count += t.pass_texts.size() - count += t.fail_texts.size() - return count + var count = 0 + for t in tests: + count += t.pass_texts.size() + count += t.fail_texts.size() + for t in setup_teardown_tests: + count += t.pass_texts.size() + count += t.fail_texts.size() + return count func get_pass_count(): - var count = 0 - for t in tests: - count += t.pass_texts.size() - for t in setup_teardown_tests: - count += t.pass_texts.size() - return count + var count = 0 + for t in tests: + count += t.pass_texts.size() + for t in setup_teardown_tests: + count += t.pass_texts.size() + return count func get_fail_count(): - var count = 0 - for t in tests: - count += t.fail_texts.size() - for t in setup_teardown_tests: - count += t.fail_texts.size() - return count + var count = 0 + for t in tests: + count += t.fail_texts.size() + for t in setup_teardown_tests: + count += t.fail_texts.size() + return count func get_pending_count(): - var count = 0 - for t in tests: - count += t.pending_texts.size() - return count + var count = 0 + for t in tests: + count += t.pending_texts.size() + return count func get_passing_test_count(): - var count = 0 - for t in tests: - if(t.is_passing()): - count += 1 - return count + var count = 0 + for t in tests: + if(t.is_passing()): + count += 1 + return count func get_failing_test_count(): - var count = 0 - for t in tests: - if(t.is_failing()): - count += 1 - return count + var count = 0 + for t in tests: + if(t.is_failing()): + count += 1 + return count func get_risky_count(): - var count = 0 - if(was_skipped): - count = 1 - else: - for t in tests: - if(t.is_risky()): - count += 1 - return count + var count = 0 + if(was_skipped): + count = 1 + else: + for t in tests: + if(t.is_risky()): + count += 1 + return count func to_s(): - var to_return = path - if(inner_class_name != null): - to_return += str('.', inner_class_name) - to_return += "\n" - for i in range(tests.size()): - to_return += str(' ', tests[i].to_s()) - return to_return + var to_return = path + if(inner_class_name != null): + to_return += str('.', inner_class_name) + to_return += "\n" + for i in range(tests.size()): + to_return += str(' ', tests[i].to_s()) + return to_return diff --git a/addons/gut/collected_test.gd b/addons/gut/collected_test.gd index 32a54b0d..6302355e 100644 --- a/addons/gut/collected_test.gd +++ b/addons/gut/collected_test.gd @@ -10,25 +10,28 @@ var has_printed_name = false # the number of arguments the method has var arg_count = 0 +# the time it took to execute the test in seconds +var time_taken : float = 0 + # The number of asserts in the test. Converted to a property for backwards # compatibility. This now reflects the text sizes instead of being a value # that can be altered externally. var assert_count = 0 : - get: return pass_texts.size() + fail_texts.size() - set(val): pass + get: return pass_texts.size() + fail_texts.size() + set(val): pass # Converted to propety for backwards compatibility. This now cannot be set # externally var pending = false : - get: return is_pending() - set(val): pass + get: return is_pending() + set(val): pass # the line number when the test fails var line_number = -1 # Set internally by Gut using whatever reason Gut wants to use to set this. # Gut will skip these marked true and the test will be listed as risky. -var should_skip = false +var should_skip = false # -- Currently not used by GUT don't believe ^ var pass_texts = [] var fail_texts = [] @@ -39,77 +42,77 @@ var was_run = false func did_pass(): - return is_passing() + return is_passing() func add_fail(fail_text): - fail_texts.append(fail_text) + fail_texts.append(fail_text) func add_pending(pending_text): - pending_texts.append(pending_text) + pending_texts.append(pending_text) func add_pass(passing_text): - pass_texts.append(passing_text) + pass_texts.append(passing_text) # must have passed an assert and not have any other status to be passing func is_passing(): - return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0 + return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0 # failing takes precedence over everything else, so any failures makes the # test a failure. func is_failing(): - return fail_texts.size() > 0 + return fail_texts.size() > 0 # test is only pending if pending was called and the test is not failing. func is_pending(): - return pending_texts.size() > 0 and fail_texts.size() == 0 + return pending_texts.size() > 0 and fail_texts.size() == 0 func is_risky(): - return should_skip or (was_run and !did_something()) + return should_skip or (was_run and !did_something()) func did_something(): - return is_passing() or is_failing() or is_pending() + return is_passing() or is_failing() or is_pending() func get_status_text(): - var to_return = GutUtils.TEST_STATUSES.NO_ASSERTS + var to_return = GutUtils.TEST_STATUSES.NO_ASSERTS - if(should_skip): - to_return = GutUtils.TEST_STATUSES.SKIPPED - elif(!was_run): - to_return = GutUtils.TEST_STATUSES.NOT_RUN - elif(pending_texts.size() > 0): - to_return = GutUtils.TEST_STATUSES.PENDING - elif(fail_texts.size() > 0): - to_return = GutUtils.TEST_STATUSES.FAILED - elif(pass_texts.size() > 0): - to_return = GutUtils.TEST_STATUSES.PASSED + if(should_skip): + to_return = GutUtils.TEST_STATUSES.SKIPPED + elif(!was_run): + to_return = GutUtils.TEST_STATUSES.NOT_RUN + elif(pending_texts.size() > 0): + to_return = GutUtils.TEST_STATUSES.PENDING + elif(fail_texts.size() > 0): + to_return = GutUtils.TEST_STATUSES.FAILED + elif(pass_texts.size() > 0): + to_return = GutUtils.TEST_STATUSES.PASSED - return to_return + return to_return # Deprecated func get_status(): - return get_status_text() + return get_status_text() func to_s(): - var pad = ' ' - var to_return = str(name, "[", get_status_text(), "]\n") - - for i in range(fail_texts.size()): - to_return += str(pad, 'Fail: ', fail_texts[i]) - for i in range(pending_texts.size()): - to_return += str(pad, 'Pending: ', pending_texts[i], "\n") - for i in range(pass_texts.size()): - to_return += str(pad, 'Pass: ', pass_texts[i], "\n") - return to_return + var pad = ' ' + var to_return = str(name, "[", get_status_text(), "]\n") + + for i in range(fail_texts.size()): + to_return += str(pad, 'Fail: ', fail_texts[i]) + for i in range(pending_texts.size()): + to_return += str(pad, 'Pending: ', pending_texts[i], "\n") + for i in range(pass_texts.size()): + to_return += str(pad, 'Pass: ', pass_texts[i], "\n") + return to_return diff --git a/addons/gut/comparator.gd b/addons/gut/comparator.gd index 48776c41..8510d67f 100644 --- a/addons/gut/comparator.gd +++ b/addons/gut/comparator.gd @@ -1,17 +1,19 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _strutils = _utils.Strutils.new() +var _strutils = GutUtils.Strutils.new() var _max_length = 100 var _should_compare_int_to_float = true const MISSING = '|__missing__gut__compare__value__|' + func _cannot_compare_text(v1, v2): return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ', _strutils.types[typeof(v2)], '.') + func _make_missing_string(text): return '' + func _create_missing_result(v1, v2, text): var to_return = null var v1_str = format_value(v1) @@ -19,10 +21,10 @@ func _create_missing_result(v1, v2, text): if(typeof(v1) == TYPE_STRING and v1 == MISSING): v1_str = _make_missing_string(text) - to_return = _utils.CompareResult.new() + to_return = GutUtils.CompareResult.new() elif(typeof(v2) == TYPE_STRING and v2 == MISSING): v2_str = _make_missing_string(text) - to_return = _utils.CompareResult.new() + to_return = GutUtils.CompareResult.new() if(to_return != null): to_return.summary = str(v1_str, ' != ', v2_str) @@ -36,7 +38,7 @@ func simple(v1, v2, missing_string=''): if(missing_result != null): return missing_result - var result = _utils.CompareResult.new() + var result = GutUtils.CompareResult.new() var cmp_str = null var extra = '' @@ -48,11 +50,11 @@ func simple(v1, v2, missing_string=''): result.are_equal = v1 == v2 elif([TYPE_STRING, TYPE_STRING_NAME].has(tv1) and [TYPE_STRING, TYPE_STRING_NAME].has(tv2)): result.are_equal = v1 == v2 - elif(_utils.are_datatypes_same(v1, v2)): + elif(GutUtils.are_datatypes_same(v1, v2)): result.are_equal = v1 == v2 if(typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY): - var sub_result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP) + var sub_result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP) result.summary = sub_result.get_short_summary() if(!sub_result.are_equal): extra = ".\n" + sub_result.get_short_summary() @@ -69,9 +71,9 @@ func simple(v1, v2, missing_string=''): func shallow(v1, v2): var result = null - if(_utils.are_datatypes_same(v1, v2)): + if(GutUtils.are_datatypes_same(v1, v2)): if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): - result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP) + result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP) else: result = simple(v1, v2) else: @@ -83,9 +85,9 @@ func shallow(v1, v2): func deep(v1, v2): var result = null - if(_utils.are_datatypes_same(v1, v2)): + if(GutUtils.are_datatypes_same(v1, v2)): if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]): - result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP) + result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP) else: result = simple(v1, v2) else: @@ -98,11 +100,11 @@ func format_value(val, max_val_length=_max_length): return _strutils.truncate_string(_strutils.type2str(val), max_val_length) -func compare(v1, v2, diff_type=_utils.DIFF.SIMPLE): +func compare(v1, v2, diff_type=GutUtils.DIFF.SIMPLE): var result = null - if(diff_type == _utils.DIFF.SIMPLE): + if(diff_type == GutUtils.DIFF.SIMPLE): result = simple(v1, v2) - elif(diff_type == _utils.DIFF.DEEP): + elif(diff_type == GutUtils.DIFF.DEEP): result = deep(v1, v2) return result diff --git a/addons/gut/diff_formatter.gd b/addons/gut/diff_formatter.gd index fd954aff..05f898f6 100644 --- a/addons/gut/diff_formatter.gd +++ b/addons/gut/diff_formatter.gd @@ -1,5 +1,4 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _strutils = _utils.Strutils.new() +var _strutils = GutUtils.Strutils.new() const INDENT = ' ' var _max_to_display = 30 const ABSOLUTE_MAX_DISPLAYED = 10000 diff --git a/addons/gut/diff_tool.gd b/addons/gut/diff_tool.gd index bc33a902..26fcfc46 100644 --- a/addons/gut/diff_tool.gd +++ b/addons/gut/diff_tool.gd @@ -5,9 +5,8 @@ enum { SIMPLE } -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _strutils = _utils.Strutils.new() -var _compare = _utils.Comparator.new() +var _strutils = GutUtils.Strutils.new() +var _compare = GutUtils.Comparator.new() var DiffTool = load('res://addons/gut/diff_tool.gd') var _value_1 = null @@ -69,7 +68,7 @@ func _init(v1,v2,diff_type=DEEP): func _find_differences(v1, v2): - if(_utils.are_datatypes_same(v1, v2)): + if(GutUtils.are_datatypes_same(v1, v2)): if(typeof(v1) == TYPE_ARRAY): _brackets = {'open':'[', 'close':']'} _desc_things = 'indexes' @@ -80,10 +79,10 @@ func _find_differences(v1, v2): _diff_dictionary(v1, v2) else: _invalidate() - _utils.get_logger().error('Only Arrays and Dictionaries are supported.') + GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.') else: _invalidate() - _utils.get_logger().error('Only Arrays and Dictionaries are supported.') + GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.') func _diff_array(a1, a2): diff --git a/addons/gut/double_templates/function_template.txt b/addons/gut/double_templates/function_template.txt index dc41248d..a58b956f 100644 --- a/addons/gut/double_templates/function_template.txt +++ b/addons/gut/double_templates/function_template.txt @@ -1,7 +1,6 @@ {func_decleration} {vararg_warning}__gutdbl.spy_on('{method_name}', {param_array}) - if(__gutdbl.should_call_super('{method_name}', {param_array})): + if(__gutdbl.is_stubbed_to_call_super('{method_name}', {param_array})): return {super_call} else: - return __gutdbl.get_stubbed_return('{method_name}', {param_array}) - + return await __gutdbl.handle_other_stubs('{method_name}', {param_array}) diff --git a/addons/gut/double_tools.gd b/addons/gut/double_tools.gd index 708f9b04..186d432c 100644 --- a/addons/gut/double_tools.gd +++ b/addons/gut/double_tools.gd @@ -23,6 +23,14 @@ func _init(values=null): gut.get_autofree().add_free(double) +func _get_stubbed_method_to_call(method_name, called_with): + var method = stubber.get_call_this(double, method_name, called_with) + if(method != null): + method = method.bindv(called_with) + return method + return method + + func from_id(inst_id): if(inst_id == -1): return null @@ -30,25 +38,29 @@ func from_id(inst_id): return instance_from_id(inst_id) -func should_call_super(method_name, called_with): +func is_stubbed_to_call_super(method_name, called_with): if(stubber != null): return stubber.should_call_super(double, method_name, called_with) else: return false +func handle_other_stubs(method_name, called_with): + if(stubber == null): + return + + var method = _get_stubbed_method_to_call(method_name, called_with) + if(method != null): + return await method.call() + else: + return stubber.get_return(double, method_name, called_with) + + func spy_on(method_name, called_with): if(spy != null): spy.add_call(double, method_name, called_with) -func get_stubbed_return(method_name, called_with): - if(stubber != null): - return stubber.get_return(double, method_name, called_with) - else: - return null - - func default_val(method_name, p_index, default_val=NO_DEFAULT_VALUE): if(stubber != null): return stubber.get_default_value(double, method_name, p_index) @@ -62,4 +74,4 @@ func vararg_warning(): "This method contains a vararg argument and the paramter count was not stubbed. " + \ "GUT adds extra parameters to this method which should fill most needs. " + \ "It is recommended that you stub param_count for this object's class to ensure " + \ - "that there are not any parameter count mismatch errors.") \ No newline at end of file + "that there are not any parameter count mismatch errors.") diff --git a/addons/gut/doubler.gd b/addons/gut/doubler.gd index ec9c5241..4fb5f01d 100644 --- a/addons/gut/doubler.gd +++ b/addons/gut/doubler.gd @@ -11,6 +11,7 @@ class PackedSceneDouble: func set_script_obj(obj): _script = obj + @warning_ignore("native_method_override") func instantiate(edit_state=0): var inst = _scene.instantiate(edit_state) var export_props = [] @@ -39,23 +40,22 @@ class PackedSceneDouble: # ------------------------------------------------------------------------------ # START Doubler # ------------------------------------------------------------------------------ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _base_script_text = _utils.get_file_as_text('res://addons/gut/double_templates/script_template.txt') -var _script_collector = _utils.ScriptCollector.new() +var _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt') +var _script_collector = GutUtils.ScriptCollector.new() # used by tests for debugging purposes. var print_source = false -var inner_class_registry = _utils.InnerClassRegistry.new() +var inner_class_registry = GutUtils.InnerClassRegistry.new() # ############### # Properties # ############### -var _stubber = _utils.Stubber.new() +var _stubber = GutUtils.Stubber.new() func get_stubber(): return _stubber func set_stubber(stubber): _stubber = stubber -var _lgr = _utils.get_logger() +var _lgr = GutUtils.get_logger() func get_logger(): return _lgr func set_logger(logger): @@ -84,19 +84,19 @@ func set_strategy(strategy): _lgr.error(str('doubler.gd: invalid double strategy ', strategy)) -var _method_maker = _utils.MethodMaker.new() +var _method_maker = GutUtils.MethodMaker.new() func get_method_maker(): return _method_maker -var _ignored_methods = _utils.OneToMany.new() +var _ignored_methods = GutUtils.OneToMany.new() func get_ignored_methods(): return _ignored_methods # ############### # Private # ############### -func _init(strategy=_utils.DOUBLE_STRATEGY.SCRIPT_ONLY): - set_logger(_utils.get_logger()) +func _init(strategy=GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY): + set_logger(GutUtils.get_logger()) _strategy = strategy @@ -111,7 +111,7 @@ func _stub_to_call_super(parsed, method_name): if(!parsed.get_method(method_name).is_eligible_for_doubling()): return - var params = _utils.StubParams.new(parsed.script_path, method_name, parsed.subpath) + var params = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath) params.to_call_super() _stubber.add_stub(params) @@ -170,7 +170,7 @@ func _create_script_no_warnings(src): prev_native_override_value = ProjectSettings.get_setting(native_method_override) ProjectSettings.set_setting(native_method_override, 0) - var DblClass = _utils.create_script_from_source(src) + var DblClass = GutUtils.create_script_from_source(src) ProjectSettings.set_setting(native_method_override, prev_native_override_value) return DblClass @@ -192,7 +192,7 @@ func _create_double(parsed, strategy, override_path, partial): else: dbl_src += _get_func_text(method.meta, path) - if(strategy == _utils.DOUBLE_STRATEGY.INCLUDE_NATIVE): + if(strategy == GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE): for method in parsed.get_super_methods(): if(_is_method_eligible_for_doubling(parsed, method)): included_methods.append(method.meta.name) @@ -207,7 +207,7 @@ func _create_double(parsed, strategy, override_path, partial): if(print_source): - print(_utils.add_line_numbers(dbl_src)) + print(GutUtils.add_line_numbers(dbl_src)) var DblClass = _create_script_no_warnings(dbl_src) if(_stubber != null): @@ -218,7 +218,7 @@ func _create_double(parsed, strategy, override_path, partial): func _stub_method_default_values(which, parsed, strategy): for method in parsed.get_local_methods(): - if(method.is_eligible_for_doubling() && !_ignored_methods.has(parsed.resource, method.meta.name)): + if(method.is_eligible_for_doubling() and !_ignored_methods.has(parsed.resource, method.meta.name)): _stubber.stub_defaults_from_meta(parsed.script_path, method.meta) @@ -259,7 +259,7 @@ func _get_func_text(method_hash, path): func _parse_script(obj): var parsed = null - if(_utils.is_inner_class(obj)): + if(GutUtils.is_inner_class(obj)): if(inner_class_registry.has(obj)): parsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj) else: @@ -304,10 +304,10 @@ func partial_double_scene(scene, strategy=_strategy): func double_gdnative(which): - return _double(which, _utils.DOUBLE_STRATEGY.INCLUDE_NATIVE) + return _double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE) func partial_double_gdnative(which): - return _partial_double(which, _utils.DOUBLE_STRATEGY.INCLUDE_NATIVE) + return _partial_double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE) func double_inner(parent, inner, strategy=_strategy): diff --git a/addons/gut/dynamic_gdscript.gd b/addons/gut/dynamic_gdscript.gd new file mode 100644 index 00000000..e3e19462 --- /dev/null +++ b/addons/gut/dynamic_gdscript.gd @@ -0,0 +1,29 @@ +@tool +var default_script_name_no_extension = 'gut_dynamic_script' +var default_script_resource_path = 'res://addons/gut/not_a_real_file/' + +var _created_script_count = 0 + + +# Creates a loaded script from the passed in source. This loaded script is +# returned unless there is an error. When an error occcurs the error number +# is returned instead. +func create_script_from_source(source, override_path=null): + _created_script_count += 1 + var r_path = str(default_script_resource_path, default_script_name_no_extension, '_', _created_script_count) + if(override_path != null): + r_path = override_path + + var DynamicScript = GDScript.new() + DynamicScript.source_code = source.dedent() + # The resource_path must be unique or Godot thinks it is trying + # to load something it has already loaded and generates an error like + # ERROR: Another resource is loaded from path 'workaround for godot + # issue #65263' (possible cyclic resource inclusion). + DynamicScript.resource_path = r_path + var result = DynamicScript.reload() + if(result != OK): + DynamicScript = result + + return DynamicScript + diff --git a/addons/gut/fonts/AnonymousPro-Bold.ttf.import b/addons/gut/fonts/AnonymousPro-Bold.ttf.import index de1351f6..a3eb4791 100644 --- a/addons/gut/fonts/AnonymousPro-Bold.ttf.import +++ b/addons/gut/fonts/AnonymousPro-Bold.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60af Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import index bdde2072..ef28dd80 100644 --- a/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import +++ b/addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/AnonymousPro-Italic.ttf.import b/addons/gut/fonts/AnonymousPro-Italic.ttf.import index ce3e5b91..1779af17 100644 --- a/addons/gut/fonts/AnonymousPro-Italic.ttf.import +++ b/addons/gut/fonts/AnonymousPro-Italic.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/AnonymousPro-Regular.ttf.import b/addons/gut/fonts/AnonymousPro-Regular.ttf.import index a567498c..1e2975b1 100644 --- a/addons/gut/fonts/AnonymousPro-Regular.ttf.import +++ b/addons/gut/fonts/AnonymousPro-Regular.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-Bold.ttf.import b/addons/gut/fonts/CourierPrime-Bold.ttf.import index cb05171d..7d60fb0a 100644 --- a/addons/gut/fonts/CourierPrime-Bold.ttf.import +++ b/addons/gut/fonts/CourierPrime-Bold.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e77 Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import index 0a9a7b77..4678c9eb 100644 --- a/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import +++ b/addons/gut/fonts/CourierPrime-BoldItalic.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8 Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-Italic.ttf.import b/addons/gut/fonts/CourierPrime-Italic.ttf.import index 89412fc9..522e2950 100644 --- a/addons/gut/fonts/CourierPrime-Italic.ttf.import +++ b/addons/gut/fonts/CourierPrime-Italic.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72 Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/CourierPrime-Regular.ttf.import b/addons/gut/fonts/CourierPrime-Regular.ttf.import index 9fde40b1..38174660 100644 --- a/addons/gut/fonts/CourierPrime-Regular.ttf.import +++ b/addons/gut/fonts/CourierPrime-Regular.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a8 Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-Bold.ttf.import b/addons/gut/fonts/LobsterTwo-Bold.ttf.import index 673d1515..7548ad04 100644 --- a/addons/gut/fonts/LobsterTwo-Bold.ttf.import +++ b/addons/gut/fonts/LobsterTwo-Bold.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a47881 Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import b/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import index 62048b0e..4b609e80 100644 --- a/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import +++ b/addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-Italic.ttf.import b/addons/gut/fonts/LobsterTwo-Italic.ttf.import index d3ca2728..5899b797 100644 --- a/addons/gut/fonts/LobsterTwo-Italic.ttf.import +++ b/addons/gut/fonts/LobsterTwo-Italic.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/fonts/LobsterTwo-Regular.ttf.import b/addons/gut/fonts/LobsterTwo-Regular.ttf.import index 9cc75421..45a12c8a 100644 --- a/addons/gut/fonts/LobsterTwo-Regular.ttf.import +++ b/addons/gut/fonts/LobsterTwo-Regular.ttf.import @@ -15,7 +15,6 @@ dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd Rendering=null antialiasing=1 generate_mipmaps=false -disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gut/gui/GutControl.gd b/addons/gut/gui/GutControl.gd index c7575f9e..395e4355 100644 --- a/addons/gut/gui/GutControl.gd +++ b/addons/gut/gui/GutControl.gd @@ -55,9 +55,8 @@ func _ready(): _gut_runner.ran_from_editor = false add_child(_gut_runner) - # Becuase of the janky _utils psuedo-global script, we cannot do all this - # in _ready. If we do this in _ready, it generates a bunch of errors. - # The errors don't matter, but it looks bad. + # TODO This might not need to be called deferred after changing GutUtils to + # an all static class. call_deferred('_post_ready') diff --git a/addons/gut/gui/GutRunner.gd b/addons/gut/gui/GutRunner.gd index 7efb9960..3b22bded 100644 --- a/addons/gut/gui/GutRunner.gd +++ b/addons/gut/gui/GutRunner.gd @@ -1,10 +1,13 @@ # ############################################################################## -# This class joins together GUT, GUT Gui, GutConfig and is the main way to -# run a test suite. +# This class joins together GUT, GUT Gui, GutConfig and is THE way to kick off a +# run of a test suite. +# +# This creates its own instance of gut.gd that it manages. You can set the +# gut.gd instance if you need to for testing. +# +# Set gut_config to an instance of a configured gut_config.gd instance prior to +# running tests. # -# This creates its own instance of gut.gd that it manages. -# Use set_gut_config to set the gut_config.gd that should be used to configure -# gut. # This will create a GUI and wire it up and apply gut_config.gd options. # # Running tests: @@ -15,6 +18,9 @@ # ############################################################################## extends Node2D +const EXIT_OK = 0 +const EXIT_ERROR = 1 + var Gut = load('res://addons/gut/gut.gd') var ResultExporter = load('res://addons/gut/result_exporter.gd') var GutConfig = load('res://addons/gut/gut_config.gd') @@ -23,8 +29,18 @@ var runner_json_path = null var result_bbcode_path = null var result_json_path = null -var _gut_config = null -var _gut = null; +var lgr = GutUtils.get_logger() +var gut_config = null + +var _hid_gut = null; +# Lazy loaded gut instance. Settable for testing purposes. +var gut = _hid_gut : + get: + if(_hid_gut == null): + _hid_gut = Gut.new() + return _hid_gut + set(val): + _hid_gut = val var _wrote_results = false # The editor runs this scene using play_custom_scene, which means we cannot @@ -38,37 +54,45 @@ var ran_from_editor = true func _ready(): + GutUtils.WarningsManager.apply_warnings_dictionary( + GutUtils.warnings_at_start) + GutUtils.LazyLoader.load_all() + # When used from the panel we have to kick off the tests ourselves b/c # there's no way I know of to interact with the scene that was run via # play_custom_scene. if(ran_from_editor): - var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') - runner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path) - result_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path) - result_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path) + _run_from_editor() + + +func _exit_tree(): + if(!_wrote_results and ran_from_editor): + _write_results_for_gut_panel() - if(_gut_config == null): - _gut_config = GutConfig.new() - _gut_config.load_options(runner_json_path) - call_deferred('run_tests') +func _run_from_editor(): + var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd') + runner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path) + result_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path) + result_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path) + if(gut_config == null): + gut_config = GutConfig.new() + gut_config.load_options(runner_json_path) -func _lazy_make_gut(): - if(_gut == null): - _gut = Gut.new() + call_deferred('run_tests') func _setup_gui(show_gui): if(show_gui): - _gui.gut = _gut - var printer = _gut.logger.get_printer('gui') + _gui.gut = gut + var printer = gut.logger.get_printer('gui') printer.set_textbox(_gui.get_textbox()) else: - _gut.logger.disable_printer('gui', true) + gut.logger.disable_printer('gui', true) _gui.visible = false - var opts = _gut_config.options + var opts = gut_config.options _gui.set_font_size(opts.font_size) _gui.set_font(opts.font_name) if(opts.font_color != null and opts.font_color.is_valid_html_color()): @@ -80,7 +104,7 @@ func _setup_gui(show_gui): _gui.use_compact_mode(opts.compact_mode) -func _write_results(): +func _write_results_for_gut_panel(): var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode() var f = FileAccess.open(result_bbcode_path, FileAccess.WRITE) if(f != null): @@ -90,55 +114,107 @@ func _write_results(): push_error('Could not save bbcode, result = ', FileAccess.get_open_error()) var exporter = ResultExporter.new() - var f_result = exporter.write_json_file(_gut, result_json_path) + # TODO this should be checked and _wrote_results should maybe not be set, or + # maybe we do not care. Whichever, it should be clear. + var _f_result = exporter.write_json_file(gut, result_json_path) _wrote_results = true -func _exit_tree(): - if(!_wrote_results and ran_from_editor): - _write_results() +func _handle_quit(should_exit, should_exit_on_success, override_exit_code=EXIT_OK): + var quitting_time = should_exit or \ + (should_exit_on_success and gut.get_fail_count() == 0) + if(!quitting_time): + if(should_exit_on_success): + lgr.log("There are failing tests, exit manually.") + _gui.use_compact_mode(false) + return -func _on_tests_finished(should_exit, should_exit_on_success): - _write_results() + # For some reason, tests fail asserting that quit was called with 0 if we + # do not do this, but everything is defaulted so I don't know why it gets + # null. + var exit_code = GutUtils.nvl(override_exit_code, EXIT_OK) - if(should_exit): - get_tree().quit() - elif(should_exit_on_success and _gut.get_fail_count() == 0): - get_tree().quit() + if(gut.get_fail_count() > 0): + exit_code = EXIT_ERROR + # Overwrite the exit code with the post_script's exit code if it is set + var post_hook_inst = gut.get_post_run_script_instance() + if(post_hook_inst != null and post_hook_inst.get_exit_code() != null): + exit_code = post_hook_inst.get_exit_code() + + quit(exit_code) -func run_tests(show_gui=true): - _lazy_make_gut() +func _end_run(override_exit_code=EXIT_OK): + if(ran_from_editor): + _write_results_for_gut_panel() + + _handle_quit(gut_config.options.should_exit, + gut_config.options.should_exit_on_success, + override_exit_code) + + +# ------------- +# Events +# ------------- +func _on_tests_finished(): + _end_run() + + +# ------------- +# Public +# ------------- +func run_tests(show_gui=true): _setup_gui(show_gui) - _gut.add_children_to = self - if(_gut.get_parent() == null): - if(_gut_config.options.gut_on_top): - _gut_layer.add_child(_gut) + if(gut_config.options.dirs.size() + gut_config.options.tests.size() == 0): + var err_text = "You do not have any directories configrued, so GUT doesn't know where to find the tests. Tell GUT where to find the tests and GUT shall run the tests." + lgr.error(err_text) + push_error(err_text) + _end_run(EXIT_ERROR) + return + + var install_check_text = GutUtils.make_install_check_text() + if(install_check_text != GutUtils.INSTALL_OK_TEXT): + print("\n\n", GutUtils.version_numbers.get_version_text()) + lgr.error(install_check_text) + push_error(install_check_text) + _end_run(EXIT_ERROR) + return + + gut.add_children_to = self + if(gut.get_parent() == null): + if(gut_config.options.gut_on_top): + _gut_layer.add_child(gut) else: - add_child(_gut) + add_child(gut) - if(ran_from_editor): - _gut.end_run.connect(_on_tests_finished.bind(_gut_config.options.should_exit, _gut_config.options.should_exit_on_success)) + gut.end_run.connect(_on_tests_finished) - _gut_config.apply_options(_gut) - var run_rest_of_scripts = _gut_config.options.unit_test_name == '' + gut_config.apply_options(gut) + var run_rest_of_scripts = gut_config.options.unit_test_name == '' - _gut.test_scripts(run_rest_of_scripts) + gut.test_scripts(run_rest_of_scripts) -func get_gut(): - _lazy_make_gut() - return _gut +func set_gut_config(which): + gut_config = which -func set_gut_config(which): - _gut_config = which +# for backwards compatibility +func get_gut(): + return gut +func quit(exit_code): + # Sometimes quitting takes a few seconds. This gives some indicator + # of what is going on. + _gui.set_title("Exiting") + await get_tree().process_frame + lgr.info(str('Exiting with code ', exit_code)) + get_tree().quit(exit_code) # ############################################################################## # The MIT License (MIT) diff --git a/addons/gut/gui/ResizeHandle.gd b/addons/gut/gui/ResizeHandle.gd index 3d46fde3..a79f6e37 100644 --- a/addons/gut/gui/ResizeHandle.gd +++ b/addons/gut/gui/ResizeHandle.gd @@ -1,8 +1,8 @@ @tool extends ColorRect # ############################################################################# -# Resize Handle control. Place onto a control. Set the orientation, then -# set the control that this should resize. Then you can resize the control +# Resize Handle control. Place onto a control. Set the orientation, then +# set the control that this should resize. Then you can resize the control # by dragging this thing around. It's pretty neat. # ############################################################################# enum ORIENTATION { @@ -23,7 +23,6 @@ var _line_color = Color(.4, .4, .4) var _active_line_color = Color(.3, .3, .3) var _invalid_line_color = Color(1, 0, 0) -var _grab_margin = 2 var _line_space = 3 var _num_lines = 8 @@ -56,30 +55,30 @@ func _gui_input(event): # Draw the lines in the corner to show where you can # drag to resize the dialog -func _draw_resize_handle_right(color): +func _draw_resize_handle_right(draw_color): var br = size for i in range(_num_lines): var start = br - Vector2(i * _line_space, 0) var end = br - Vector2(0, i * _line_space) - draw_line(start, end, color, _line_width, true) + draw_line(start, end, draw_color, _line_width, true) -func _draw_resize_handle_left(color): +func _draw_resize_handle_left(draw_color): var bl = Vector2(0, size.y) - + for i in range(_num_lines): var start = bl + Vector2(i * _line_space, 0) var end = bl - Vector2(0, i * _line_space) - draw_line(start, end, color, _line_width, true) + draw_line(start, end, draw_color, _line_width, true) func _handle_right_input(event : InputEvent): if(event is InputEventMouseMotion): - if(_mouse_down and - event.global_position.x > 0 and + if(_mouse_down and + event.global_position.x > 0 and event.global_position.y < DisplayServer.window_get_size().y): - + if(vertical_resize): resize_control.size.y += event.relative.y resize_control.size.x += event.relative.x @@ -91,15 +90,15 @@ func _handle_right_input(event : InputEvent): func _handle_left_input(event : InputEvent): if(event is InputEventMouseMotion): - if(_mouse_down and - event.global_position.x > 0 and + if(_mouse_down and + event.global_position.x > 0 and event.global_position.y < DisplayServer.window_get_size().y): - + var start_size = resize_control.size resize_control.size.x -= event.relative.x if(resize_control.size.x != start_size.x): resize_control.global_position.x += event.relative.x - + if(vertical_resize): resize_control.size.y += event.relative.y elif(event is InputEventMouseButton): diff --git a/addons/gut/gui/ResultsTree.gd b/addons/gut/gui/ResultsTree.gd index c063811d..f512a5cf 100644 --- a/addons/gut/gui/ResultsTree.gd +++ b/addons/gut/gui/ResultsTree.gd @@ -274,12 +274,17 @@ func _load_result_tree(j): # 'nothing to see here' should be visible. clear_centered_text() + var add_count = 0 for key in script_keys: if(scripts[key]['props']['tests'] > 0): + add_count += 1 _add_script_to_tree(key, scripts[key]) _free_childless_scripts() - _show_all_passed() + if(add_count == 0): + add_centered_text('Nothing was run') + else: + _show_all_passed() # ------------------- diff --git a/addons/gut/gui/gut_gui.gd b/addons/gut/gui/gut_gui.gd index 68eb2c2b..6846fb45 100644 --- a/addons/gut/gui/gut_gui.gd +++ b/addons/gut/gui/gut_gui.gd @@ -30,19 +30,10 @@ var _title_mouse = { down = false } -var _resize_mouse = { - down = false -} - -var _resize_left_mouse = { - down = false -} - signal switch_modes() var _max_position = Vector2(100, 100) -var _utils = null func _ready(): _populate_ctrls() @@ -183,7 +174,7 @@ func next_script(path, num_tests): _ctrls.path_file.text = path.get_file() -func next_test(test_name): +func next_test(__test_name): _ctrls.prog_test.value += 1 diff --git a/addons/gut/gut.gd b/addons/gut/gut.gd index 60569803..34bd1d4d 100644 --- a/addons/gut/gut.gd +++ b/addons/gut/gut.gd @@ -1,4 +1,5 @@ extends 'res://addons/gut/gut_to_move.gd' +class_name GutMain # ############################################################################## # @@ -152,7 +153,7 @@ var parameter_handler = _parameter_handler : _parameter_handler = val _parameter_handler.set_logger(_lgr) -var _lgr = _utils.get_logger() +var _lgr = GutUtils.get_logger() # Local reference for the common logger. ## FOR INERNAL USE ONLY var logger = _lgr : @@ -179,31 +180,31 @@ var treat_error_as_failure = _treat_error_as_failure: # ------------ # Read only # ------------ -var _test_collector = _utils.TestCollector.new() +var _test_collector = GutUtils.TestCollector.new() func get_test_collector(): return _test_collector # var version = null : func get_version(): - return _utils.version + return GutUtils.version_numbers.gut_version -var _orphan_counter = _utils.OrphanCounter.new() +var _orphan_counter = GutUtils.OrphanCounter.new() func get_orphan_counter(): return _orphan_counter -var _autofree = _utils.AutoFree.new() +var _autofree = GutUtils.AutoFree.new() func get_autofree(): return _autofree -var _stubber = _utils.Stubber.new() +var _stubber = GutUtils.Stubber.new() func get_stubber(): return _stubber -var _doubler = _utils.Doubler.new() +var _doubler = GutUtils.Doubler.new() func get_doubler(): return _doubler -var _spy = _utils.Spy.new() +var _spy = GutUtils.Spy.new() func get_spy(): return _spy @@ -218,18 +219,19 @@ func is_running(): var _should_print_versions = true # used to cut down on output in tests. var _should_print_summary = true -var _test_prefix = 'test_' var _file_prefix = 'test_' var _inner_class_prefix = 'Test' var _select_script = '' var _last_paint_time = 0.0 -var _strutils = _utils.Strutils.new() +var _strutils = GutUtils.Strutils.new() # The instance that is created from _pre_run_script. Accessible from -# get_pre_run_script_instance. +# get_pre_run_script_instance. These are created at the start of the run +# and then referenced at the appropriate time. This allows us to validate the +# scripts prior to running. var _pre_run_script_instance = null -var _post_run_script_instance = null # This is not used except in tests. +var _post_run_script_instance = null var _script_name = null @@ -237,7 +239,6 @@ var _script_name = null var _test_script_objects = [] var _waiting = false -var _done = false # msecs ticks when run was started var _start_time = 0.0 @@ -247,8 +248,6 @@ var _current_test = null var _pause_before_teardown = false -var _awaiter = _utils.Awaiter.new() - # Used to cancel importing scripts if an error has occurred in the setup. This # prevents tests from being run if they were exported and ensures that the # error displayed is seen since importing generates a lot of text. @@ -264,7 +263,7 @@ var _auto_queue_free_delay = .1 # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ func _init(): - # When running tests for GUT itself, _utils has been setup to always return + # When running tests for GUT itself, GutUtils has been setup to always return # a new logger so this does not set the gut instance on the base logger # when creating test instances of GUT. _lgr.set_gut(self) @@ -285,18 +284,10 @@ func _init(): # Initialize controls # ------------------------------------------------------------------------------ func _ready(): - if(!_utils.is_version_ok()): - _print_versions() - push_error(_utils.get_bad_version_text()) - print('Error: ', _utils.get_bad_version_text()) - get_tree().quit() - return - if(_should_print_versions): + _lgr.log('--- GUT ---') _lgr.info(str('using [', OS.get_user_data_dir(), '] for temporary output.')) - add_child(_awaiter) - if(_select_script != null): select_script(_select_script) @@ -307,20 +298,18 @@ func _ready(): # ------------------------------------------------------------------------------ func _notification(what): if(what == NOTIFICATION_PREDELETE): - for test_script in _test_script_objects: - if(is_instance_valid(test_script)): - test_script.free() + for ts in _test_script_objects: + if(is_instance_valid(ts)): + ts.free() _test_script_objects = [] - if(is_instance_valid(_awaiter)): - _awaiter.free() func _print_versions(send_all = true): if(!_should_print_versions): return - var info = _utils.get_version_text() + var info = GutUtils.version_numbers.get_version_text() if(send_all): p(info) @@ -379,7 +368,7 @@ func _log_test_children_warning(test_script): return var kids = test_script.get_children() - if(kids.size() > 0): + if(kids.size() > 1): var msg = '' if(_log_level == 2): msg = "Test script still has children when all tests finisehd.\n" @@ -389,13 +378,12 @@ func _log_test_children_warning(test_script): else: msg = str("Test script has ", kids.size(), " unfreed children. Increase log level for more details.") - _lgr.warn(msg) func _log_end_run(): if(_should_print_summary): - var summary = _utils.Summary.new(self) + var summary = GutUtils.Summary.new(self) summary.log_end_run() @@ -426,7 +414,7 @@ func _validate_hook_script(path): # ------------------------------------------------------------------------------ # Runs a hook script. Script must exist, and must extend -# res://addons/gut/hook_script.gd +# GutHookScript or addons/gut/hook_script.gd # ------------------------------------------------------------------------------ func _run_hook_script(inst): if(inst != null): @@ -461,7 +449,7 @@ func _end_run(): _log_end_run() _is_running = false - _run_hook_script(_post_run_script_instance) + _run_hook_script(get_post_run_script_instance()) _export_results() end_run.emit() @@ -476,7 +464,7 @@ func _export_results(): # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ func _export_junit_xml(): - var exporter = _utils.JunitXmlExport.new() + var exporter = GutUtils.JunitXmlExport.new() var output_file = _junit_xml_file if(_junit_xml_timestamp): @@ -506,12 +494,29 @@ func _does_class_name_match(the_class_name, script_class_name): # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ -func _setup_script(test_script): +func _setup_script(test_script, collected_script): test_script.gut = self test_script.set_logger(_lgr) _add_children_to.add_child(test_script) _test_script_objects.append(test_script) + if(!test_script._was_ready_called): + test_script._do_ready_stuff() + _lgr.warn(str("!!! YOU HAVE UPSET YOUR GUT !!!\n", + "You have overridden _ready in [", collected_script.get_filename_and_inner(), "] ", + "but it does not call super._ready(). New additions (or maybe old ", + "by the time you see this) require that super._ready() is called.", + "\n\n", + "GUT is working around this infraction, but may not be able to in ", + "the future. GUT also reserves the right to decide it does not want ", + "to work around it in the future. ", + "You should probably use before_all instead of _ready. I can think ", + "of a few reasons why you would want to use _ready but I won't list ", + "them here because I think they are bad ideas. I know they are bad ", + "ideas because I did them. Hence the warning. This message is ", + "intentially long so that it bothers you and you change your ways.\n\n", + "Thank you for using GUT.")) + # ------------------------------------------------------------------------------ # returns self so it can be integrated into the yield call. @@ -524,10 +529,10 @@ func _wait_for_continue_button(): # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ -func _get_indexes_matching_script_name(name): +func _get_indexes_matching_script_name(script_name): var indexes = [] # empty runs all for i in range(_test_collector.scripts.size()): - if(_test_collector.scripts[i].get_filename().find(name) != -1): + if(_test_collector.scripts[i].get_filename().find(script_name) != -1): indexes.append(i) return indexes @@ -571,7 +576,6 @@ func _run_test(script_inst, test_name): _lgr.log_test_name() _lgr.set_indent_level(1) _orphan_counter.add_counter('test') - var script_result = null await script_inst.before_each() @@ -611,7 +615,7 @@ func _run_test(script_inst, test_name): # Calls both pre-all-tests methods until prerun_setup is removed # ------------------------------------------------------------------------------ func _call_before_all(test_script, collected_script): - var before_all_test_obj = _utils.CollectedTest.new() + var before_all_test_obj = GutUtils.CollectedTest.new() before_all_test_obj.has_printed_name = false before_all_test_obj.name = 'before_all' @@ -636,7 +640,7 @@ func _call_before_all(test_script, collected_script): # Calls both post-all-tests methods until postrun_teardown is removed. # ------------------------------------------------------------------------------ func _call_after_all(test_script, collected_script): - var after_all_test_obj = _utils.CollectedTest.new() + var after_all_test_obj = GutUtils.CollectedTest.new() after_all_test_obj.has_printed_name = false after_all_test_obj.name = 'after_all' @@ -653,11 +657,42 @@ func _call_after_all(test_script, collected_script): _current_test = null +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func _should_skip_script(test_script, collected_script): + var skip_message = 'not skipped' + var skip_value = test_script.get('skip_script') + var should_skip = false + + if(skip_value == null): + skip_value = test_script.should_skip_script() + else: + _lgr.deprecated('Using the skip_script var has been deprecated. Implement the new should_skip_script() method in your test instead.') + + if(skip_value != null): + if(typeof(skip_value) == TYPE_BOOL): + should_skip = skip_value + if(skip_value): + skip_message = 'script marked to skip' + elif(typeof(skip_value) == TYPE_STRING): + should_skip = true + skip_message = skip_value + + if(should_skip): + var msg = str('- [Script skipped]: ', skip_message) + _lgr.inc_indent() + _lgr.log(msg, _lgr.fmts.yellow) + _lgr.dec_indent() + collected_script.skip_reason = skip_message + collected_script.was_skipped = true + + return should_skip + # ------------------------------------------------------------------------------ # Run all tests in a script. This is the core logic for running tests. # ------------------------------------------------------------------------------ func _test_the_scripts(indexes=[]): - _orphan_counter.add_counter('total') + _orphan_counter.add_counter('pre_run') _print_versions(false) var is_valid = _init_run() @@ -665,7 +700,7 @@ func _test_the_scripts(indexes=[]): _lgr.error('Something went wrong and the run was aborted.') return - _run_hook_script(_pre_run_script_instance) + _run_hook_script(get_pre_run_script_instance()) if(_pre_run_script_instance!= null and _pre_run_script_instance.should_abort()): _lgr.error('pre-run abort') end_run.emit() @@ -699,24 +734,16 @@ func _test_the_scripts(indexes=[]): var test_script = coll_script.get_new() + _setup_script(test_script, coll_script) + _doubler.set_strategy(_double_strategy) + # ---- # SHORTCIRCUIT # skip_script logic - var skip_script = test_script.get('skip_script') - if(skip_script != null): - var msg = str('- [Script skipped]: ', skip_script) - _lgr.inc_indent() - _lgr.log(msg, _lgr.fmts.yellow) - _lgr.dec_indent() - coll_script.skip_reason = skip_script - coll_script.was_skipped = true + if(_should_skip_script(test_script, coll_script)): continue # ---- - var script_result = null - _setup_script(test_script) - _doubler.set_strategy(_double_strategy) - # !!! # 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 @@ -729,37 +756,33 @@ func _test_the_scripts(indexes=[]): await _call_before_all(test_script, coll_script) # Each test in the script - var skip_suffix = '_skip__' - coll_script.mark_tests_to_skip_with_suffix(skip_suffix) for i in range(coll_script.tests.size()): _stubber.clear() _spy.clear() _current_test = coll_script.tests[i] - script_result = null - - # ------------------ - # SHORTCIRCUI - if(_current_test.should_skip): - continue - # ------------------ if((_unit_test_name != '' and _current_test.name.find(_unit_test_name) > -1) or (_unit_test_name == '')): + var ticks_before := Time.get_ticks_usec() + if(_current_test.arg_count > 1): _lgr.error(str('Parameterized test ', _current_test.name, ' has too many parameters: ', _current_test.arg_count, '.')) elif(_current_test.arg_count == 1): _current_test.was_run = true - script_result = await _run_parameterized_test(test_script, _current_test.name) + await _run_parameterized_test(test_script, _current_test.name) else: _current_test.was_run = true - script_result = await _run_test(test_script, _current_test.name) + await _run_test(test_script, _current_test.name) if(!_current_test.did_something()): _lgr.risky(str(_current_test.name, ' did not assert')) _current_test.has_printed_name = false + + _current_test.time_taken = (Time.get_ticks_usec() - ticks_before) / 1000000.0 + end_test.emit() # After each test, check to see if we shoudl wait a frame to @@ -794,6 +817,11 @@ func _test_the_scripts(indexes=[]): # END TEST SCRIPT LOOP _lgr.set_indent_level(0) + # Give anything that is queued to be freed time to be freed before we count + # the orphans. Without this, the last test's awaiter won't be freed + # yet, which messes with the orphans total. There could also be objects + # the user has queued to be freed as well. + await get_tree().create_timer(.1).timeout _end_run() @@ -916,7 +944,7 @@ func _get_files(path, prefix, suffix): # public # ######################### -func get_elapsed_time(): +func get_elapsed_time() -> float: var to_return = 0.0 if(_start_time != 0.0): to_return = Time.get_ticks_msec() - _start_time @@ -948,13 +976,14 @@ func p(text, level=0): # ------------------------------------------------------------------------------ # Runs all the scripts that were added using add_script # ------------------------------------------------------------------------------ -func test_scripts(run_rest=false): +func test_scripts(_run_rest=false): if(_script_name != null and _script_name != ''): var indexes = _get_indexes_matching_script_name(_script_name) if(indexes == []): _lgr.error(str( "Could not find script matching '", _script_name, "'.\n", "Check your directory settings and Script Prefix/Suffix settings.")) + end_run.emit() else: _test_the_scripts(indexes) else: @@ -968,11 +997,11 @@ func run_tests(run_rest=false): # ------------------------------------------------------------------------------ # 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() +# func run_test_script(script): +# _test_collector.set_test_class_prefix(_inner_class_prefix) +# _test_collector.clear() +# _test_collector.add_script(script) +# _test_the_scripts() # ------------------------------------------------------------------------------ @@ -1035,7 +1064,7 @@ func export_tests(path=_export_path): # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ func import_tests(path=_export_path): - if(!_utils.file_exists(path)): + if(!FileAccess.file_exists(path)): _lgr.error(str('Cannot import tests: the path [', path, '] does not exist.')) else: _test_collector.clear() @@ -1117,34 +1146,6 @@ func pause_before_teardown(): _pause_before_teardown = true; -# ------------------------------------------------------------------------------ -# Uses the awaiter to wait for x amount of time. The signal emitted when the -# time has expired is returned (_awaiter.timeout). -# ------------------------------------------------------------------------------ -func set_wait_time(time, text=''): - _awaiter.wait_for(time) - _lgr.yield_msg(str('-- Awaiting ', time, ' second(s) -- ', text)) - return _awaiter.timeout - - -# ------------------------------------------------------------------------------ -# Uses the awaiter to wait for x frames. The signal emitted is returned. -# ------------------------------------------------------------------------------ -func set_wait_frames(frames, text=''): - _awaiter.wait_frames(frames) - _lgr.yield_msg(str('-- Awaiting ', frames, ' frame(s) -- ', text)) - return _awaiter.timeout - - -# ------------------------------------------------------------------------------ -# Wait for a signal or a maximum amount of time. The signal emitted is returned. -# ------------------------------------------------------------------------------ -func set_wait_for_signal_or_time(obj, signal_name, max_wait, text=''): - _awaiter.wait_for_signal(Signal(obj, signal_name), max_wait) - _lgr.yield_msg(str('-- Awaiting signal "', signal_name, '" or for ', max_wait, ' second(s) -- ', text)) - return _awaiter.timeout - - # ------------------------------------------------------------------------------ # Returns the script object instance that is currently being run. # ------------------------------------------------------------------------------ @@ -1164,7 +1165,7 @@ func get_current_test_object(): ## Returns a summary.gd object that contains all the information about ## the run results. func get_summary(): - return _utils.Summary.new(self) + return GutUtils.Summary.new(self) # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ @@ -1182,10 +1183,20 @@ func show_orphans(should): _lgr.set_type_enabled(_lgr.types.orphan, should) +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ func get_logger(): return _lgr +# ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +func get_test_script_count(): + return _test_script_objects.size() + + + + # ############################################################################## # The MIT License (MIT) # ===================== diff --git a/addons/gut/gut_cmdln.gd b/addons/gut/gut_cmdln.gd index 48d67603..7327d671 100644 --- a/addons/gut/gut_cmdln.gd +++ b/addons/gut/gut_cmdln.gd @@ -2,313 +2,15 @@ # Description # ----------- # Command line interface for the GUT unit testing tool. Allows you to run tests -# from the command line instead of running a scene. Place this script along with -# gut.gd into your scripts directory at the root of your project. Once there you -# can run this script (from the root of your project) using the following command: -# godot -s -d test/gut/gut_cmdln.gd +# from the command line instead of running a scene. You can run this script +# (from the root of your project) using the following command: +# godot -s test/gut/gut_cmdln.gd # -# See the readme for a list of options and examples. You can also use the -gh +# See the wiki 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. # ------------------------------------------------------------------------------ extends SceneTree -var Optparse = load('res://addons/gut/optparse.gd') -var Gut = load('res://addons/gut/gut.gd') -var GutRunner = load('res://addons/gut/gui/GutRunner.tscn') - -var json = JSON.new() - -# ------------------------------------------------------------------------------ -# Helper class to resolve the various different places where an option can -# be set. Using the get_value method will enforce the order of precedence of: -# 1. command line value -# 2. config file value -# 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 precedented hashes -# will punch through null values of higher precedented hashes. -# ------------------------------------------------------------------------------ -class OptionResolver: - var base_opts = {} - var cmd_opts = {} - var config_opts = {} - - - func get_value(key): - return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key])) - - func set_base_opts(opts): - base_opts = opts - cmd_opts = _null_copy(opts) - config_opts = _null_copy(opts) - - # creates a copy of a hash with all values null. - func _null_copy(h): - var new_hash = {} - for key in h: - new_hash[key] = null - return new_hash - - func _nvl(a, b): - if(a == null): - return b - else: - return a - func _string_it(h): - var to_return = '' - for key in h: - to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')') - return to_return - - func to_s(): - return str("base:\n", _string_it(base_opts), "\n", \ - "config:\n", _string_it(config_opts), "\n", \ - "cmd:\n", _string_it(cmd_opts), "\n", \ - "resolved:\n", _string_it(get_resolved_values())) - - func get_resolved_values(): - var to_return = {} - for key in base_opts: - to_return[key] = get_value(key) - return to_return - - func to_s_verbose(): - var to_return = '' - var resolved = get_resolved_values() - for key in base_opts: - to_return += str(key, "\n") - to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n") - to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n") - to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n") - to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n") - - return to_return - -# ------------------------------------------------------------------------------ -# Here starts the actual script that uses the Options class to kick off Gut -# and run your tests. -# ------------------------------------------------------------------------------ -var _utils = null -var _gut_config = load('res://addons/gut/gut_config.gd').new() -# instance of gut -var _tester = null -# array of command line options specified -var _final_opts = [] - - -func setup_options(options, font_names): - var opts = Optparse.new() - opts.set_banner( -""" -The GUT CLI ------------ -The default behavior for GUT is to load options from a res://.gutconfig.json if -it exists. Any options specified on the command line will take precedence over -options specified in the gutconfig file. You can specify a different gutconfig -file with the -gconfig option. - -To generate a .gutconfig.json file you can use -gprint_gutconfig_sample -To see the effective values of a CLI command and a gutconfig use -gpo - -Any option that requires a value will take the form of \"-g=\". -There cannot be any spaces between the option, the \"=\", or ' + 'inside a -specified value or godot will think you are trying to run a scene. -""") - # Run specific things - opts.add('-gselect', '', ('All scripts that contain the specified string in their filename will be ran')) - opts.add('-ginner_class', '', 'Only run inner classes that contain the specified string int their name.') - opts.add('-gunit_test_name', '', ('Any test that contains the specified text will be run, all others will be skipped.')) - - # Run Config - opts.add('-ginclude_subdirs', false, 'Include subdirectories of -gdir.') - opts.add('-gdir', options.dirs, 'Comma delimited list of directories to add tests from.') - opts.add('-gtest', [], 'Comma delimited list of full paths to test scripts to run.') - opts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir. Default "[default]".') - opts.add('-gsuffix', options.suffix, 'Test script suffix, including .gd extension. Default "[default]".') - opts.add('-gconfig', 'res://.gutconfig.json', 'A config file that contains configuration information. Default is res://.gutconfig.json') - opts.add('-gpre_run_script', '', 'pre-run hook script path') - opts.add('-gpost_run_script', '', 'post-run hook script path') - opts.add('-gerrors_do_not_cause_failure', false, 'When an internal GUT error occurs tests will fail. With this option set, that does not happen.') - opts.add('-gdouble_strategy', 'SCRIPT_ONLY', 'Default strategy to use when doubling. Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY]. Default "[default]"') - - # Misc - opts.add('-gpaint_after', options.paint_after, 'Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]') - - # Display options - opts.add('-glog', options.log_level, 'Log level. Default [default]') - opts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts. Default "[default]".') - opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.') - opts.add('-gcompact_mode', false, 'The runner will be in compact mode. This overrides -gmaximize.') - opts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.') - opts.add('-gdisable_colors', false, 'Disable command line colors.') - opts.add('-gfont_name', options.font_name, str('Valid values are: ', font_names, '. Default "[default]"')) - opts.add('-gfont_size', options.font_size, 'Font size, default "[default]"') - opts.add('-gbackground_color', options.background_color, 'Background color as an html color, default "[default]"') - opts.add('-gfont_color',options.font_color, 'Font color as an html color, default "[default]"') - - # End Behavior - opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.') - opts.add('-gexit_on_success', false, 'Only exit if all tests pass.') - opts.add('-gignore_pause', false, 'Ignores any calls to gut.pause_before_teardown.') - - # Helpish options - opts.add('-gh', false, 'Print this help. You did this to see this, so you probably understand.') - opts.add('-gpo', false, 'Print option values from all sources and the value used.') - opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file.') - - # Output options - opts.add('-gjunit_xml_file', options.junit_xml_file, 'Export results of run to this file in the Junit XML format.') - opts.add('-gjunit_xml_timestamp', options.junit_xml_timestamp, 'Include a timestamp in the -gjunit_xml_file, 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.config_file = from.get_value('-gconfig') - to.dirs = from.get_value('-gdir') - to.disable_colors = from.get_value('-gdisable_colors') - to.double_strategy = from.get_value('-gdouble_strategy') - to.ignore_pause = from.get_value('-gignore_pause') - to.include_subdirs = from.get_value('-ginclude_subdirs') - to.inner_class = from.get_value('-ginner_class') - to.log_level = from.get_value('-glog') - to.opacity = from.get_value('-gopacity') - to.post_run_script = from.get_value('-gpost_run_script') - to.pre_run_script = from.get_value('-gpre_run_script') - to.prefix = from.get_value('-gprefix') - to.selected = from.get_value('-gselect') - to.should_exit = from.get_value('-gexit') - to.should_exit_on_success = from.get_value('-gexit_on_success') - to.should_maximize = from.get_value('-gmaximize') - to.compact_mode = from.get_value('-gcompact_mode') - to.hide_orphans = from.get_value('-ghide_orphans') - to.suffix = from.get_value('-gsuffix') - to.errors_do_not_cause_failure = from.get_value('-gerrors_do_not_cause_failure') - to.tests = from.get_value('-gtest') - to.unit_test_name = from.get_value('-gunit_test_name') - - to.font_size = from.get_value('-gfont_size') - to.font_name = from.get_value('-gfont_name') - to.background_color = from.get_value('-gbackground_color') - to.font_color = from.get_value('-gfont_color') - to.paint_after = from.get_value('-gpaint_after') - - to.junit_xml_file = from.get_value('-gjunit_xml_file') - to.junit_xml_timestamp = from.get_value('-gjunit_xml_timestamp') - - - -func _print_gutconfigs(values): - var header = """Here is a sample of a full .gutconfig.json file. -You do not need to specify all values in your own file. The values supplied in -this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample -option (option priority: command-line, .gutconfig, default).""" - print("\n", header.replace("\n", ' '), "\n\n") - var resolved = values - - # remove_at some options that don't make sense to be in config - resolved.erase("config_file") - resolved.erase("show_help") - - print("Here's a config with all the properties set based off of your current command and config.") - print(json.stringify(resolved, ' ')) - - for key in resolved: - resolved[key] = null - - print("\n\nAnd here's an empty config for you fill in what you want.") - print(json.stringify(resolved, ' ')) - - -# parse options and run Gut -func _run_gut(): - var opt_resolver = OptionResolver.new() - opt_resolver.set_base_opts(_gut_config.default_options) - - print("\n\n", ' --- Gut ---') - var o = setup_options(_gut_config.default_options, _gut_config.valid_fonts) - - var all_options_valid = o.parse() - extract_command_line_options(o, opt_resolver.cmd_opts) - - var load_result = _gut_config.load_options_no_defaults( - opt_resolver.get_value('config_file')) - - # SHORTCIRCUIT - if(!all_options_valid or load_result == -1): - _end_run(1) - else: - opt_resolver.config_opts = _gut_config.options - - if(o.get_value('-gh')): - print(_utils.get_version_text()) - o.print_help() - _end_run(0) - elif(o.get_value('-gpo')): - print('All command line options and where they are specified. ' + - 'The "final" value shows which value will actually be used ' + - 'based on order of precedence (default < .gutconfig < cmd line).' + "\n") - print(opt_resolver.to_s_verbose()) - _end_run(0) - elif(o.get_value('-gprint_gutconfig_sample')): - _print_gutconfigs(opt_resolver.get_resolved_values()) - _end_run(0) - else: - _final_opts = opt_resolver.get_resolved_values(); - _gut_config.options = _final_opts - - var runner = GutRunner.instantiate() - - runner.ran_from_editor = false - runner.set_gut_config(_gut_config) - - get_root().add_child(runner) - _tester = runner.get_gut() - _tester.connect('end_run', Callable(self,'_on_tests_finished').bind(_final_opts.should_exit, _final_opts.should_exit_on_success)) - - run_tests(runner) - - -func run_tests(runner): - runner.run_tests() - - -func _end_run(exit_code=-9999): - if(is_instance_valid(_utils)): - _utils.free() - - if(exit_code != -9999): - quit(exit_code) - -# exit if option is set. -func _on_tests_finished(should_exit, should_exit_on_success): - if(_final_opts.dirs.size() == 0): - if(_tester.get_summary().get_totals().scripts == 0): - var lgr = _tester.logger - lgr.error('No directories configured. Add directories with options or a .gutconfig.json file. Use the -gh option for more information.') - - var exit_code = 0 - if(_tester.get_fail_count()): - exit_code = 1 - - # Overwrite the exit code with the post_script - var post_inst = _tester.get_post_run_script_instance() - if(post_inst != null and post_inst.get_exit_code() != null): - exit_code = post_inst.get_exit_code() - - if(should_exit or (should_exit_on_success and _tester.get_fail_count() == 0)): - _end_run(exit_code) - else: - _end_run() - print("Tests finished, exit manually") - - -# ------------------------------------------------------------------------------ -# MAIN -# ------------------------------------------------------------------------------ func _init(): var max_iter = 20 var iter = 0 @@ -323,13 +25,9 @@ func _init(): quit(0) return - _utils = GutUtils.get_instance() - if(!_utils.is_version_ok()): - print("\n\n", _utils.get_version_text()) - push_error(_utils.get_bad_version_text()) - _end_run(1) - else: - _run_gut() + var cli = load('res://addons/gut/cli/gut_cli.gd').new() + get_root().add_child(cli) + cli.main() # ############################################################################## diff --git a/addons/gut/gut_config.gd b/addons/gut/gut_config.gd index a0d5aa5a..a9fbeefc 100644 --- a/addons/gut/gut_config.gd +++ b/addons/gut/gut_config.gd @@ -4,10 +4,8 @@ # to a json file. It is also responsible for applying these settings to GUT. # # ############################################################################## -var Gut = load('res://addons/gut/gut.gd') - - var valid_fonts = ['AnonymousPro', 'CourierPro', 'LobsterTwo', 'Default'] + var default_options = { background_color = Color(.15, .15, .15, 1).to_html(), config_file = 'res://.gutconfig.json', @@ -53,7 +51,6 @@ var default_options = { var options = default_options.duplicate() -var json = JSON.new() func _null_copy(h): @@ -87,8 +84,6 @@ func _load_options_from_config_file(file_path, into): # SHORTCIRCUIT if(results == null): print("\n\n",'!! ERROR parsing file: ', file_path) - print(' at line ', results.error_line, ':') - print(' ', results.error_string) return -1 # Get all the options out of the config file using the option name. The @@ -146,7 +141,7 @@ func _apply_options(opts, gut): # Public # -------------------------- func write_options(path): - var content = json.stringify(options, ' ') + var content = JSON.stringify(options, ' ') var f = FileAccess.open(path, FileAccess.WRITE) var result = FileAccess.get_open_error() diff --git a/addons/gut/gut_to_move.gd b/addons/gut/gut_to_move.gd index 4b28ff73..925f2817 100644 --- a/addons/gut/gut_to_move.gd +++ b/addons/gut/gut_to_move.gd @@ -1,7 +1,6 @@ # Temporary base script for gut.gd to hold the things to be remvoed and added # to some utility somewhere. extends Node -var _utils = load('res://addons/gut/utils.gd').get_instance() # ------------------------------------------------------------------------------ # deletes all files in a given directory @@ -51,7 +50,7 @@ func is_file_empty(path): # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ func get_file_as_text(path): - return _utils.get_file_as_text(path) + return GutUtils.get_file_as_text(path) # ------------------------------------------------------------------------------ # Creates an empty file at the specified path diff --git a/addons/gut/input_sender.gd b/addons/gut/input_sender.gd index 26b666d9..1c9a1b66 100644 --- a/addons/gut/input_sender.gd +++ b/addons/gut/input_sender.gd @@ -73,7 +73,7 @@ class InputQueueItem: if(frame_delay > 0 and _delay_started): _waited_frames += 1 if(_waited_frames >= frame_delay): - emit_signal("event_ready") + event_ready.emit() func _init(t_delay,f_delay): time_delay = t_delay @@ -82,7 +82,7 @@ class InputQueueItem: func _on_time_timeout(): _is_ready = true - emit_signal("event_ready") + event_ready.emit() func _delay_timer(t): return Engine.get_main_loop().root.get_tree().create_timer(t) @@ -182,12 +182,11 @@ class MouseDraw: # ############################################################################## # # ############################################################################## -var _utils = load('res://addons/gut/utils.gd').get_instance() var InputFactory = load("res://addons/gut/input_factory.gd") const INPUT_WARN = 'If using Input as a reciever it will not respond to *_down events until a *_up event is recieved. Call the appropriate *_up event or use hold_for(...) to automatically release after some duration.' -var _lgr = _utils.get_logger() +var _lgr = GutUtils.get_logger() var _receivers = [] var _input_queue = [] var _next_queue_item = null @@ -277,6 +276,11 @@ func _send_event(event): for r in _receivers: if(r == Input): Input.parse_input_event(event) + if(event is InputEventAction): + if(event.pressed): + Input.action_press(event.action) + else: + Input.action_release(event.action) if(_auto_flush_input): Input.flush_buffered_events() else: @@ -322,6 +326,9 @@ func _new_defaulted_mouse_button_event(position, global_position): func _new_defaulted_mouse_motion_event(position, global_position): var event = InputEventMouseMotion.new() _apply_last_position_and_set_last_position(event, position, global_position) + for key in _pressed_mouse_buttons: + if(_pressed_mouse_buttons[key].pressed): + event.button_mask += key return event @@ -337,7 +344,7 @@ func _on_queue_item_ready(item): if(_input_queue.size() == 0): _next_queue_item = null - emit_signal("idle") + idle.emit() else: _input_queue[0].start() diff --git a/addons/gut/junit_xml_export.gd b/addons/gut/junit_xml_export.gd index 116cfdf9..7e438969 100644 --- a/addons/gut/junit_xml_export.gd +++ b/addons/gut/junit_xml_export.gd @@ -1,9 +1,7 @@ # ------------------------------------------------------------------------------ # Creates an export of a test run in the JUnit XML format. # ------------------------------------------------------------------------------ -var _utils = load('res://addons/gut/utils.gd').get_instance() - -var _exporter = _utils.ResultExporter.new() +var _exporter = GutUtils.ResultExporter.new() func indent(s, ind): var to_return = ind + s @@ -39,7 +37,8 @@ func _export_tests(script_result, classname): to_return += add_attr("name", key) to_return += add_attr("assertions", assert_count) to_return += add_attr("status", test.status) - to_return += add_attr("classname", classname) + to_return += add_attr("classname", classname.replace("res://", "")) + to_return += add_attr("time", test.time_taken) to_return += ">\n" to_return += _export_test_result(test) @@ -48,16 +47,25 @@ func _export_tests(script_result, classname): return to_return +func _sum_test_time(script_result, classname)->float: + var to_return := 0.0 + + for key in script_result.keys(): + var test = script_result[key] + to_return += test.time_taken + + return to_return func _export_scripts(exp_results): var to_return = "" for key in exp_results.test_scripts.scripts.keys(): var s = exp_results.test_scripts.scripts[key] to_return += "= 0 and type_flag < _supported_defaults.size() and _supported_defaults[type_flag] != null @@ -117,11 +116,11 @@ func _is_supported_default(type_flag): func _make_stub_default(method, index): return str('__gutdbl.default_val("', method, '",', index, ')') + func _make_arg_array(method_meta, override_size): var to_return = [] var has_unsupported_defaults = false - var dflt_start = method_meta.args.size() - method_meta.default_args.size() for i in range(method_meta.args.size()): var pname = method_meta.args[i].name @@ -253,10 +252,9 @@ func get_function_text(meta, override_size=null): return text - - func get_logger(): return _lgr + func set_logger(logger): _lgr = logger diff --git a/addons/gut/optparse.gd b/addons/gut/optparse.gd deleted file mode 100644 index f54988e0..00000000 --- a/addons/gut/optparse.gd +++ /dev/null @@ -1,252 +0,0 @@ -# ############################################################################## -#(G)odot (U)nit (T)est class -# -# ############################################################################## -# The MIT License (MIT) -# ===================== -# -# Copyright (c) 2020 Tom "Butch" Wesley -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ############################################################################## -# Description -# ----------- -# Command line interface for the GUT unit testing tool. Allows you to run tests -# from the command line instead of running a scene. Place this script along with -# gut.gd into your scripts directory at the root of your project. Once there you -# can run this script (from the root of your project) using the following command: -# godot -s -d test/gut/gut_cmdln.gd -# -# 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. -# ############################################################################## - -#------------------------------------------------------------------------------- -# Parses the command line arguments supplied into an array that can then be -# examined and parsed based on how the gut options work. -#------------------------------------------------------------------------------- -class CmdLineParser: - var _used_options = [] - # an array of arrays. Each element in this array will contain an option - # name and if that option contains a value then it will have a sedond - # element. For example: - # [[-gselect, test.gd], [-gexit]] - var _opts = [] - - func _init(): - for i in range(OS.get_cmdline_args().size()): - var opt_val = OS.get_cmdline_args()[i].split('=') - _opts.append(opt_val) - - # Parse out multiple comma delimited values from a command line - # option. Values are separated from option name with "=" and - # additional values are comma separated. - func _parse_array_value(full_option): - var value = _parse_option_value(full_option) - var split = value.split(',') - return split - - # Parse out the value of an option. Values are separated from - # the option name with "=" - func _parse_option_value(full_option): - if(full_option.size() > 1): - return full_option[1] - else: - return null - - # Search _opts for an element that starts with the option name - # specified. - func find_option(name): - var found = false - var idx = 0 - - while(idx < _opts.size() and !found): - if(_opts[idx][0] == name): - found = true - else: - idx += 1 - - if(found): - return idx - else: - return -1 - - func get_array_value(option): - _used_options.append(option) - var to_return = [] - var opt_loc = find_option(option) - if(opt_loc != -1): - to_return = _parse_array_value(_opts[opt_loc]) - _opts.remove_at(opt_loc) - - return to_return - - # returns the value of an option if it was specified, null otherwise. This - # used to return the default but that became problemnatic when trying to - # punch through the different places where values could be specified. - func get_value(option): - _used_options.append(option) - var to_return = null - var opt_loc = find_option(option) - if(opt_loc != -1): - to_return = _parse_option_value(_opts[opt_loc]) - _opts.remove_at(opt_loc) - - return to_return - - # returns true if it finds the option, false if not. - func was_specified(option): - _used_options.append(option) - return find_option(option) != -1 - - # Returns any unused command line options. I found that only the -s and - # script name come through from godot, all other options that godot uses - # are not sent through OS.get_cmdline_args(). - # - # This is a onetime thing b/c i kill all items in _used_options - func get_unused_options(): - var to_return = [] - for i in range(_opts.size()): - to_return.append(_opts[i][0]) - - var script_option = to_return.find("-s") - if script_option == -1: - script_option = to_return.find("--script") - if script_option != -1: - to_return.remove_at(script_option + 1) - to_return.remove_at(script_option) - - while(_used_options.size() > 0): - var index = to_return.find(_used_options[0].split("=")[0]) - if(index != -1): - to_return.remove_at(index) - _used_options.remove_at(0) - - return to_return - -#------------------------------------------------------------------------------- -# Simple class to hold a command line option -#------------------------------------------------------------------------------- -class Option: - var value = null - var option_name = '' - var default = null - var description = '' - - func _init(name,default_value,desc=''): - option_name = name - default = default_value - description = desc - value = null#default_value - - func pad(to_pad, size, pad_with=' '): - var to_return = to_pad - for _i in range(to_pad.length(), size): - to_return += pad_with - - return to_return - - func to_s(min_space=0): - var subbed_desc = description - if(subbed_desc.find('[default]') != -1): - subbed_desc = subbed_desc.replace('[default]', str(default)) - return pad(option_name, min_space) + subbed_desc - -#------------------------------------------------------------------------------- -# The high level interface between this script and the command line options -# supplied. Uses Option class and CmdLineParser to extract information from -# the command line and make it easily accessible. -#------------------------------------------------------------------------------- -var options = [] -var _opts = [] -var _banner = '' - -func add(name, default, desc): - options.append(Option.new(name, default, desc)) - -func get_value(name): - var found = false - var idx = 0 - - while(idx < options.size() and !found): - if(options[idx].option_name == name): - found = true - else: - idx += 1 - - if(found): - return options[idx].value - else: - print("COULD NOT FIND OPTION " + name) - return null - -func set_banner(banner): - _banner = banner - -func print_help(): - var longest = 0 - for i in range(options.size()): - if(options[i].option_name.length() > longest): - longest = options[i].option_name.length() - - print('---------------------------------------------------------') - print(_banner) - - print("\nOptions\n-------") - for i in range(options.size()): - print(' ' + options[i].to_s(longest + 2)) - print('---------------------------------------------------------') - -func print_options(): - for i in range(options.size()): - print(options[i].option_name + '=' + str(options[i].value)) - -func parse(): - var parser = CmdLineParser.new() - - for i in range(options.size()): - var t = typeof(options[i].default) - # only set values that were specified at the command line so that - # we can punch through default and config values correctly later. - # Without this check, you can't tell the difference between the - # defaults and what was specified, so you can't punch through - # higher level options. - if(parser.was_specified(options[i].option_name)): - if(t == TYPE_INT): - options[i].value = int(parser.get_value(options[i].option_name)) - elif(t == TYPE_STRING): - options[i].value = parser.get_value(options[i].option_name) - elif(t == TYPE_ARRAY): - options[i].value = parser.get_array_value(options[i].option_name) - elif(t == TYPE_BOOL): - options[i].value = parser.was_specified(options[i].option_name) - elif(t == TYPE_FLOAT): - options[i].value = parser.get_value(options[i].option_name) - elif(t == TYPE_NIL): - print(options[i].option_name + ' cannot be processed, it has a nil datatype') - else: - print(options[i].option_name + ' cannot be processed, it has unknown datatype:' + str(t)) - - var unused = parser.get_unused_options() - if(unused.size() > 0): - print("Unrecognized options: ", unused) - return false - - return true diff --git a/addons/gut/orphan_counter.gd b/addons/gut/orphan_counter.gd index b31e2b6f..366ba92f 100644 --- a/addons/gut/orphan_counter.gd +++ b/addons/gut/orphan_counter.gd @@ -1,3 +1,46 @@ +# ------------------------------------------------------------------------------ +# This is used to track the change in orphans over different intervals. +# You use this by adding a counter at the start of an interval and then +# using get_orphans_since to find out how many orphans have been created since +# that counter was added. +# +# For example, when a test starts, gut adds a counter for "test" which +# creates/sets the counter's value to the current orphan count. At the end of +# the test GUT uses get_orphans_since("test") to find out how many orphans +# were created by the test. +# ------------------------------------------------------------------------------ +var _counters = {} + +func orphan_count(): + return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) + +func add_counter(name): + _counters[name] = orphan_count() + +# Returns the number of orphans created since add_counter was last called for +# the name. Returns -1 to avoid blowing up with an invalid name but still +# be somewhat visible that we've done something wrong. +func get_orphans_since(name): + return orphan_count() - _counters[name] if _counters.has(name) else -1 + +func get_count(name): + return _counters.get(name, -1) + +func print_orphans(name, lgr): + var count = get_orphans_since(name) + + if(count > 0): + var o = 'orphan' + if(count > 1): + o = 'orphans' + lgr.orphan(str(count, ' new ', o, ' in ', name, '.')) + +func print_all(): + var msg = str("Total Orphans ", orphan_count(), "\n", JSON.stringify(_counters, " ")) + print(msg) + + + # ############################################################################## #(G)odot (U)nit (T)est class # @@ -5,7 +48,7 @@ # The MIT License (MIT) # ===================== # -# Copyright (c) 2020 Tom "Butch" Wesley +# Copyright (c) 2024 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 @@ -30,26 +73,4 @@ # add_counter is called it adds/resets the value in the dictionary to the # current number of orphans. Each call to get_counter will return the change # in orphans since add_counter was last called. -# ############################################################################## -var _counters = {} - -func orphan_count(): - return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) - -func add_counter(name): - _counters[name] = orphan_count() - -# Returns the number of orphans created since add_counter was last called for -# the name. Returns -1 to avoid blowing up with an invalid name but still -# be somewhat visible that we've done something wrong. -func get_counter(name): - return orphan_count() - _counters[name] if _counters.has(name) else -1 - -func print_orphans(name, lgr): - var count = get_counter(name) - - if(count > 0): - var o = 'orphan' - if(count > 1): - o = 'orphans' - lgr.orphan(str(count, ' new ', o, ' in ', name, '.')) +# ############################################################################## \ No newline at end of file diff --git a/addons/gut/parameter_handler.gd b/addons/gut/parameter_handler.gd index 6b37e82c..794d6934 100644 --- a/addons/gut/parameter_handler.gd +++ b/addons/gut/parameter_handler.gd @@ -1,11 +1,10 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() var _params = null var _call_count = 0 var _logger = null func _init(params=null): _params = params - _logger = _utils.get_logger() + _logger = GutUtils.get_logger() if(typeof(_params) != TYPE_ARRAY): _logger.error('You must pass an array to parameter_handler constructor.') _params = null diff --git a/addons/gut/plugin.cfg b/addons/gut/plugin.cfg index 510f4046..d6ce8f3d 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="9.2.1" +version="9.3.0" script="gut_plugin.gd" diff --git a/addons/gut/result_exporter.gd b/addons/gut/result_exporter.gd index e685fdb4..d085dada 100644 --- a/addons/gut/result_exporter.gd +++ b/addons/gut/result_exporter.gd @@ -4,7 +4,6 @@ # of a run and exporting it in a specific format. This can also serve as a # unofficial GUT export format. # ------------------------------------------------------------------------------ -var _utils = load('res://addons/gut/utils.gd').get_instance() var json = JSON.new() func _export_tests(collected_script): @@ -17,7 +16,8 @@ func _export_tests(collected_script): "passing":test.pass_texts, "failing":test.fail_texts, "pending":test.pending_texts, - "orphans":test.orphans + "orphans":test.orphans, + "time_taken": test.time_taken } return to_return @@ -61,9 +61,6 @@ func _make_results_dict(): return result -# TODO -# time -# errors func get_results_dictionary(gut, include_scripts=true): var scripts = [] @@ -82,7 +79,7 @@ func get_results_dictionary(gut, include_scripts=true): props.errors = gut.logger.get_errors().size() props.warnings = gut.logger.get_warnings().size() props.time = gut.get_elapsed_time() - props.orphans = gut.get_orphan_counter().get_counter('total') + props.orphans = gut.get_orphan_counter().get_orphans_since('pre_run') result.test_scripts.scripts = scripts return result @@ -90,12 +87,12 @@ func get_results_dictionary(gut, include_scripts=true): func write_json_file(gut, path): var dict = get_results_dictionary(gut) - var json_text = json.stringify(dict, ' ') + var json_text = JSON.stringify(dict, ' ') - var f_result = _utils.write_file(path, json_text) + var f_result = GutUtils.write_file(path, json_text) if(f_result != OK): var msg = str("Error: ", f_result, ". Could not create export file ", path) - _utils.get_logger().error(msg) + GutUtils.get_logger().error(msg) return f_result @@ -103,11 +100,11 @@ func write_json_file(gut, path): func write_summary_file(gut, path): var dict = get_results_dictionary(gut, false) - var json_text = json.stringify(dict, ' ') + var json_text = JSON.stringify(dict, ' ') - var f_result = _utils.write_file(path, json_text) + var f_result = GutUtils.write_file(path, json_text) if(f_result != OK): var msg = str("Error: ", f_result, ". Could not create export file ", path) - _utils.get_logger().error(msg) + GutUtils.get_logger().error(msg) return f_result diff --git a/addons/gut/script_parser.gd b/addons/gut/script_parser.gd index b2f37d54..ff5a556e 100644 --- a/addons/gut/script_parser.gd +++ b/addons/gut/script_parser.gd @@ -19,11 +19,10 @@ class ParsedMethod: var meta = _meta : get: return _meta set(val): return; - var is_local = false + var is_local = false var _parameters = [] - func _init(metadata): _meta = metadata var start_default = _meta.args.size() - _meta.default_args.size() @@ -76,7 +75,6 @@ class ParsedMethod: class ParsedScript: # All methods indexed by name. var _methods_by_name = {} - var _utils = load('res://addons/gut/utils.gd').get_instance() var _script_path = null var script_path = _script_path : @@ -93,24 +91,27 @@ class ParsedScript: get: return _resource set(val): return; - var _native_instance = null - var is_native = false : - get: return _native_instance != null + var _is_native = false + var is_native = _is_native: + get: return _is_native set(val): return; - func unreference(): - if(_native_instance != null): - _native_instance.free() - return super() + var _native_methods = {} + var _native_class_name = "" + func _init(script_or_inst, inner_class=null): var to_load = script_or_inst - if(_utils.is_native_class(to_load)): + if(GutUtils.is_native_class(to_load)): _resource = to_load - _native_instance = to_load.new() + _is_native = true + var inst = to_load.new() + _native_class_name = inst.get_class() + _native_methods = inst.get_method_list() + inst.free() else: if(!script_or_inst is Resource): to_load = load(script_or_inst.get_script().get_path()) @@ -129,14 +130,14 @@ class ParsedScript: func _print_flags(meta): - print(str(meta.name, ':').rpad(30), str(meta.flags).rpad(4), ' = ', _utils.dec2bistr(meta.flags, 10)) + print(str(meta.name, ':').rpad(30), str(meta.flags).rpad(4), ' = ', GutUtils.dec2bistr(meta.flags, 10)) func _get_native_methods(base_type): var to_return = [] if(base_type != null): var source = str('extends ', base_type) - var inst = _utils.create_script_from_source(source).new() + var inst = GutUtils.create_script_from_source(source).new() to_return = inst.get_method_list() if(! inst is RefCounted): inst.free() @@ -146,7 +147,7 @@ class ParsedScript: func _parse_methods(thing): var methods = [] if(is_native): - methods = _native_instance.get_method_list() + methods = _native_methods.duplicate() else: var base_type = thing.get_instance_base_type() methods = _get_native_methods(base_type) @@ -260,7 +261,7 @@ class ParsedScript: func get_extends_text(): var text = null if(is_native): - text = str("extends ", _native_instance.get_class()) + text = str("extends ", _native_class_name) else: text = str("extends '", _script_path, "'") if(_subpath != null): @@ -271,13 +272,11 @@ class ParsedScript: # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ var scripts = {} -var _utils = load('res://addons/gut/utils.gd').get_instance() - func _get_instance_id(thing): var inst_id = null - if(_utils.is_native_class(thing)): + if(GutUtils.is_native_class(thing)): var id_str = str(thing).replace("<", '').replace(">", '').split('#')[1] inst_id = id_str.to_int() elif(typeof(thing) == TYPE_STRING): @@ -307,7 +306,7 @@ func parse(thing, inner_thing=null): if(inner_thing != null): inner = instance_from_id(_get_instance_id(inner_thing)) - if(obj is Resource or _utils.is_native_class(obj)): + if(obj is Resource or GutUtils.is_native_class(obj)): parsed = ParsedScript.new(obj, inner) scripts[key] = parsed diff --git a/addons/gut/signal_watcher.gd b/addons/gut/signal_watcher.gd index 0193bc14..47626760 100644 --- a/addons/gut/signal_watcher.gd +++ b/addons/gut/signal_watcher.gd @@ -52,8 +52,7 @@ const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_' # - some_signal on ref2 was never emitted. # - other_signal on ref2 was emitted 3 times, each time with 3 parameters. var _watched_signals = {} -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() +var _lgr = GutUtils.get_logger() func _add_watched_signal(obj, name): # SHORTCIRCUIT - ignore dupes @@ -131,7 +130,7 @@ func watch_signal(object, signal_name): _add_watched_signal(object, signal_name) did = true else: - _utils.get_logger().warn(str(object, ' does not have signal ', signal_name)) + GutUtils.get_logger().warn(str(object, ' does not have signal ', signal_name)) return did func get_emit_count(object, signal_name): @@ -170,7 +169,7 @@ func is_watching(object, signal_name): func clear(): for obj in _watched_signals: - if(_utils.is_not_freed(obj)): + if(GutUtils.is_not_freed(obj)): for signal_name in _watched_signals[obj]: obj.disconnect(signal_name, Callable(self,'_on_watched_signal')) _watched_signals.clear() @@ -208,7 +207,7 @@ func get_signal_summary(obj): func print_signal_summary(obj): if(!is_watching_object(obj)): var msg = str('Not watching signals for ', obj) - _utils.get_logger().warn(msg) + GutUtils.get_logger().warn(msg) return var summary = get_signal_summary(obj) diff --git a/addons/gut/spy.gd b/addons/gut/spy.gd index 2aef4add..d2fd6223 100644 --- a/addons/gut/spy.gd +++ b/addons/gut/spy.gd @@ -9,9 +9,8 @@ # }, # } var _calls = {} -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() -var _compare = _utils.Comparator.new() +var _lgr = GutUtils.get_logger() +var _compare = GutUtils.Comparator.new() func _find_parameters(call_params, params_to_find): var found = false diff --git a/addons/gut/strutils.gd b/addons/gut/strutils.gd index 51e45cce..3a9be4d3 100644 --- a/addons/gut/strutils.gd +++ b/addons/gut/strutils.gd @@ -1,12 +1,12 @@ class_name GutStringUtils -var _utils = load('res://addons/gut/utils.gd').get_instance() # Hash containing all the built in types in Godot. This provides an English # name for the types that corosponds with the type constants defined in the # engine. var types = {} func _init_types_dictionary(): + types[TYPE_NIL] = 'NIL' types[TYPE_AABB] = 'AABB' types[TYPE_ARRAY] = 'ARRAY' types[TYPE_BASIS] = 'BASIS' @@ -68,11 +68,11 @@ func _get_obj_filename(thing): var filename = null if(thing == null or - _utils.is_native_class(thing) or + GutUtils.is_native_class(thing) or !is_instance_valid(thing) or str(thing) == '' or typeof(thing) != TYPE_OBJECT or - _utils.is_double(thing)): + GutUtils.is_double(thing)): return if(thing.get_script() == null): @@ -82,7 +82,7 @@ func _get_obj_filename(thing): # If it isn't a packed scene and it doesn't have a script then # we do nothing. This just reads better. pass - elif(!_utils.is_native_class(thing)): + elif(!GutUtils.is_native_class(thing)): var dict = inst_to_dict(thing) filename = _get_filename(dict['@path']) if(str(dict['@subpath']) != ''): @@ -116,9 +116,9 @@ func type2str(thing): # better this way. pass elif(typeof(thing) == TYPE_OBJECT): - if(_utils.is_native_class(thing)): - str_thing = _utils.get_native_class_name(thing) - elif(_utils.is_double(thing)): + if(GutUtils.is_native_class(thing)): + str_thing = GutUtils.get_native_class_name(thing) + elif(GutUtils.is_double(thing)): var double_path = _get_filename(thing.__gutdbl.thepath) if(thing.__gutdbl.subpath != ''): double_path += str('/', thing.__gutdbl.subpath) diff --git a/addons/gut/stub_params.gd b/addons/gut/stub_params.gd index ee4543f1..d24d90d7 100644 --- a/addons/gut/stub_params.gd +++ b/addons/gut/stub_params.gd @@ -1,5 +1,8 @@ -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() + +var _lgr = GutUtils.get_logger() +var logger = _lgr : + get: return _lgr + set(val): _lgr = val var return_val = null var stub_target = null @@ -7,6 +10,7 @@ var stub_target = null var parameters = null var stub_method = null var call_super = false +var call_this = null # Whether this is a stub for default parameter values as they are defined in # the script, and not an overridden default value. var is_script_default = false @@ -29,11 +33,17 @@ var _parameter_override_only = true const NOT_SET = '|_1_this_is_not_set_1_|' -func _init(target=null,method=null,subpath=null): +func _init(target=null, method=null, _subpath=null): stub_target = target stub_method = method - if(typeof(target) == TYPE_STRING): + if(typeof(target) == TYPE_CALLABLE): + stub_target = target.get_object() + stub_method = target.get_method() + parameters = target.get_bound_arguments() + if(parameters.size() == 0): + parameters = null + elif(typeof(target) == TYPE_STRING): if(target.is_absolute_path()): stub_target = load(str(target)) else: @@ -48,6 +58,7 @@ func _init(target=null,method=null,subpath=null): if(typeof(method) == TYPE_DICTIONARY): _load_defaults_from_metadata(method) + func _load_defaults_from_metadata(meta): stub_method = meta.name var values = meta.default_args.duplicate() @@ -56,6 +67,7 @@ func _load_defaults_from_metadata(meta): param_defaults(values) + func to_return(val): if(stub_method == '_init'): _lgr.error("You cannot stub _init to do nothing. Super's _init is always called.") @@ -77,6 +89,11 @@ func to_call_super(): return self +func to_call(callable : Callable): + call_this = callable + return self + + func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET): parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10] var idx = 0 @@ -104,10 +121,10 @@ func has_param_override(): func is_param_override_only(): - var to_return = false + var ret_val = false if(has_param_override()): - to_return = _parameter_override_only - return to_return + ret_val = _parameter_override_only + return ret_val func to_s(): @@ -124,6 +141,9 @@ func to_s(): if(call_super): base_string += " to call SUPER" + if(call_this != null): + base_string += str(" to call ", call_this) + if(parameters != null): base_string += str(' with params (', parameters, ') returns ', return_val) else: diff --git a/addons/gut/stubber.gd b/addons/gut/stubber.gd index acd111f8..cd735f12 100644 --- a/addons/gut/stubber.gd +++ b/addons/gut/stubber.gd @@ -12,9 +12,8 @@ # } # } var returns = {} -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() -var _strutils = _utils.Strutils.new() +var _lgr = GutUtils.get_logger() +var _strutils = GutUtils.Strutils.new() var _class_db_name_hash = {} func _init(): @@ -33,7 +32,7 @@ func _make_crazy_dynamic_over_engineered_class_db_hash(): else: text += str('# ', classname, "\n") text += "}" - var inst = _utils.create_script_from_source(text).new() + var inst = GutUtils.create_script_from_source(text).new() return inst.all_the_classes @@ -46,7 +45,7 @@ func _find_matches(obj, method): # an entry for the instance then see if we have an entry for the class. if(returns.has(obj) and returns[obj].has(method)): matches = returns[obj][method] - elif(_utils.is_instance(obj)): + elif(GutUtils.is_instance(obj)): var parent = obj.get_script() var found = false while(parent != null and !found): @@ -174,6 +173,13 @@ func should_call_super(obj, method, parameters=null): return should +func get_call_this(obj, method, parameters=null): + var stub_info = _find_stub(obj, method, parameters) + + if(stub_info != null): + return stub_info.call_this + + func get_parameter_count(obj, method): var to_return = null var stub_info = _find_stub(obj, method, null, true) @@ -225,6 +231,6 @@ func to_s(): func stub_defaults_from_meta(target, method_meta): - var params = _utils.StubParams.new(target, method_meta) + var params = GutUtils.StubParams.new(target, method_meta) params.is_script_default = true add_stub(params) diff --git a/addons/gut/summary.gd b/addons/gut/summary.gd index 8bbf9a40..c037af11 100644 --- a/addons/gut/summary.gd +++ b/addons/gut/summary.gd @@ -21,26 +21,31 @@ func _log_end_run_header(gut): func _log_what_was_run(gut): - if(!gut._utils.is_null_or_empty(gut._select_script)): + if(!GutUtils.is_null_or_empty(gut._select_script)): gut.p('Ran Scripts matching "' + gut._select_script + '"') - if(!gut._utils.is_null_or_empty(gut._unit_test_name)): + if(!GutUtils.is_null_or_empty(gut._unit_test_name)): gut.p('Ran Tests matching "' + gut._unit_test_name + '"') - if(!gut._utils.is_null_or_empty(gut._inner_class_name)): + if(!GutUtils.is_null_or_empty(gut._inner_class_name)): gut.p('Ran Inner Classes matching "' + gut._inner_class_name + '"') func _log_orphans_and_disclaimer(gut): - var orphan_count = gut.get_orphan_counter() var lgr = gut.get_logger() - # Do not count any of the _test_script_objects since these will be released - # when GUT is released. - orphan_count._counters.total += gut._test_script_objects.size() - if(orphan_count.get_counter('total') > 0 and lgr.is_type_enabled('orphan')): - orphan_count.print_orphans('total', lgr) + if(!lgr.is_type_enabled('orphan')): + return + + var counter = gut.get_orphan_counter() + # Do not count any of the test scripts since these will be released when GUT + # is released. + var do_not_count_orphans = counter.get_count("pre_run") + gut.get_test_script_count() + var total_run_orphans = counter.orphan_count() - do_not_count_orphans + + if(total_run_orphans > 0): + lgr.orphan(str("Total orphans in run ", total_run_orphans)) gut.p("Note: This count does not include GUT objects that will be freed upon exit.") gut.p(" It also does not include any orphans created by global scripts") gut.p(" loaded before tests were ran.") - gut.p(str("Total orphans = ", orphan_count.orphan_count())) + gut.p(str("Total orphans = ", counter.orphan_count())) gut.p('') @@ -58,12 +63,12 @@ func _log_non_zero_total(text, value, lgr): else: return 0 + func _log_totals(gut, totals): var lgr = gut.get_logger() lgr.log() lgr.log("---- Totals ----") - var col1 = 18 var issue_count = 0 issue_count += _log_non_zero_total('Errors', totals.errors, lgr) issue_count += _log_non_zero_total('Warnings', totals.warnings, lgr) @@ -82,6 +87,12 @@ func _log_totals(gut, totals): return totals +func _log_nothing_run(gut): + var lgr = gut.get_logger() + lgr.error("Nothing was run.") + lgr.log('On the one hand nothing failed, on the other hand nothing did anything.') + + # --------------------- # Public # --------------------- @@ -186,10 +197,12 @@ func get_totals(gut=_gut): func log_end_run(gut=_gut): - _log_end_run_header(gut) - var totals = get_totals(gut) - var tc = gut.get_test_collector() + if(totals.tests == 0): + _log_nothing_run(gut) + return + + _log_end_run_header(gut) var lgr = gut.get_logger() log_all_non_passing_tests(gut) diff --git a/addons/gut/test.gd b/addons/gut/test.gd index 3eeae5ea..c1f34634 100644 --- a/addons/gut/test.gd +++ b/addons/gut/test.gd @@ -31,22 +31,19 @@ class_name GutTest # # Version - see gut.gd # ############################################################################## -# Class that all test scripts must extend. +# Class that all test scripts must extend.` # # This provides all the asserts and other testing features. Test scripts are # run by the Gut class in gut.gd # ############################################################################## extends Node - -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _compare = _utils.Comparator.new() +var _compare = GutUtils.Comparator.new() # 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 -# access to the asserts in the tests you write. -var gut = null +# is set by the gut class when it runs the test script. +var gut: GutMain = null var _disable_strict_datatype_checks = false # Holds all the text for a test's fail/pass. This is used for testing purposes @@ -68,26 +65,47 @@ 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 +# Convenience copy of GutUtils.DOUBLE_STRATEGY var DOUBLE_STRATEGY = GutUtils.DOUBLE_STRATEGY -var _lgr = _utils.get_logger() -var _strutils = _utils.Strutils.new() +var _lgr = GutUtils.get_logger() +var _strutils = GutUtils.Strutils.new() +var _awaiter = null # syntax sugar -var ParameterFactory = _utils.ParameterFactory -var CompareResult = _utils.CompareResult -var InputFactory = _utils.InputFactory -var InputSender = _utils.InputSender +var ParameterFactory = GutUtils.ParameterFactory +var CompareResult = GutUtils.CompareResult +var InputFactory = GutUtils.InputFactory +var InputSender = GutUtils.InputSender -func _init(): - pass + +var _was_ready_called = false +# I haven't decided if we should be using _ready or not. Right now gut.gd will +# call this if _ready was not called (because it was overridden without a super +# call). Maybe gut.gd should just call _do_ready_stuff (after we rename it to +# something better). I'm leaving all this as it is until it bothers me more. +func _do_ready_stuff(): + _awaiter = GutUtils.Awaiter.new() + add_child(_awaiter) + _was_ready_called = true + + +func _ready(): + _do_ready_stuff() + + +func _notification(what): + # Tests are never expected to re-enter the tree. Tests are removed from the + # tree after they are run. + if(what == NOTIFICATION_EXIT_TREE): + _awaiter.queue_free() func _str(thing): return _strutils.type2str(thing) + func _str_precision(value, precision): var to_return = _str(value) var format = str('%.', precision, 'f') @@ -210,7 +228,7 @@ func _fail_if_parameters_not_array(parameters): func _get_bad_double_or_method_message(inst, method_name, what_you_cant_do): var to_return = '' - if(!_utils.is_double(inst)): + if(!GutUtils.is_double(inst)): to_return = str("An instance of a Double was expected, you passed: ", _str(inst)) elif(!inst.has_method(method_name)): to_return = str("You cannot ", what_you_cant_do, " [", method_name, "] because the method does not exist. ", @@ -255,19 +273,22 @@ func _create_obj_from_type(type): # Virtual Methods # ####################### -# alias for prerun_setup +func should_skip_script(): + return false + + func before_all(): pass -# alias for setup + func before_each(): pass -# alias for postrun_teardown + func after_all(): pass -# alias for teardown + func after_each(): pass @@ -380,6 +401,17 @@ func assert_gt(got, expected, text=""): else: _fail(disp) +# ------------------------------------------------------------------------------ +# Asserts got is greater than or equal to expected +# ------------------------------------------------------------------------------ +func assert_gte(got, expected, text=""): + var disp = "[" + _str(got) + "] expected to be >= than [" + _str(expected) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text)): + if(got >= expected): + _pass(disp) + else: + _fail(disp) + # ------------------------------------------------------------------------------ # Asserts got is less than expected # ------------------------------------------------------------------------------ @@ -391,6 +423,17 @@ func assert_lt(got, expected, text=""): else: _fail(disp) +# ------------------------------------------------------------------------------ +# Asserts got is less than or equal to expected +# ------------------------------------------------------------------------------ +func assert_lte(got, expected, text=""): + var disp = "[" + _str(got) + "] expected to be <= than [" + _str(expected) + "]: " + text + if(_do_datatypes_match__fail_if_not(got, expected, text)): + if(got <= expected): + _pass(disp) + else: + _fail(disp) + # ------------------------------------------------------------------------------ # asserts that got is true # ------------------------------------------------------------------------------ @@ -757,7 +800,7 @@ func get_signal_parameters(object, signal_name, index=-1): # ------------------------------------------------------------------------------ func get_call_parameters(object, method_name, index=-1): var to_return = null - if(_utils.is_double(object)): + if(GutUtils.is_double(object)): to_return = gut.get_spy().get_call_parameters(object, method_name, index) else: _lgr.error('You must pass a doulbed object to get_call_parameters.') @@ -787,7 +830,7 @@ func assert_is(object, a_class, text=''): else: var a_str = _str(a_class) disp = str('Expected [', _str(object), '] to extend [', a_str, ']: ', text) - if(!_utils.is_native_class(a_class) and !_utils.is_gdscript(a_class)): + if(!GutUtils.is_native_class(a_class) and !GutUtils.is_gdscript(a_class)): _fail(str(bad_param_2, a_str)) else: if(is_instance_of(object, a_class)): @@ -834,10 +877,13 @@ func assert_not_typeof(object, type, text=''): # The match_case flag determines case sensitivity. # ------------------------------------------------------------------------------ func assert_string_contains(text, search, match_case=true): - var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.' + const empty_search = 'Expected text and search strings to be non-empty. You passed %s and %s.' + const non_strings = 'Expected text and search to both be strings. You passed %s and %s.' var disp = 'Expected \'%s\' to contain \'%s\', match_case=%s' % [text, search, match_case] - if(text == '' or search == ''): - _fail(empty_search % [text, search]) + if(typeof(text) != TYPE_STRING or typeof(search) != TYPE_STRING): + _fail(non_strings % [_str(text), _str(search)]) + elif(text == '' or search == ''): + _fail(empty_search % [_str(text), _str(search)]) elif(match_case): if(text.find(search) == -1): _fail(disp) @@ -1000,7 +1046,7 @@ func assert_not_freed(obj, title): # the last thing your test does. # ------------------------------------------------------------------------------ func assert_no_new_orphans(text=''): - var count = gut.get_orphan_counter().get_counter('test') + var count = gut.get_orphan_counter().get_orphans_since('test') var msg = '' if(text != ''): msg = ': ' + text @@ -1143,7 +1189,6 @@ func assert_property(obj, property_name, default_value, new_value) -> void: _warn_for_public_accessors(obj, property_name) - # ------------------------------------------------------------------------------ # Mark the current test as pending. # ------------------------------------------------------------------------------ @@ -1159,29 +1204,30 @@ func pending(text=""): # Gut detects the yield. # ------------------------------------------------------------------------------ func wait_seconds(time, msg=''): - var to_return = gut.set_wait_time(time, msg) - return to_return + _lgr.yield_msg(str('-- Awaiting ', time, ' second(s) -- ', msg)) + _awaiter.wait_seconds(time) + return _awaiter.timeout func yield_for(time, msg=''): _lgr.deprecated('yield_for', 'wait_seconds') - var to_return = gut.set_wait_time(time, msg) - return to_return + return wait_seconds(time, msg) # ------------------------------------------------------------------------------ # Yield to a signal or a maximum amount of time, whichever comes first. # ------------------------------------------------------------------------------ -func wait_for_signal(sig, max_wait, msg=''): +func wait_for_signal(sig : Signal, max_wait, msg=''): watch_signals(sig.get_object()) - var to_return = gut.set_wait_for_signal_or_time(sig.get_object(), sig.get_name(), max_wait, msg) - return to_return + _lgr.yield_msg(str('-- Awaiting signal "', sig.get_name(), '" or for ', max_wait, ' second(s) -- ', msg)) + _awaiter.wait_for_signal(sig, max_wait) + await _awaiter.timeout + return !_awaiter.did_last_wait_timeout func yield_to(obj, signal_name, max_wait, msg=''): _lgr.deprecated('yield_to', 'wait_for_signal') - watch_signals(obj) - var to_return = gut.set_wait_for_signal_or_time(obj, signal_name, max_wait, msg) - return to_return + return await wait_for_signal(Signal(obj, signal_name), max_wait, msg) + # ------------------------------------------------------------------------------ # Yield for a number of frames. The optional message will be printed. when @@ -1193,14 +1239,35 @@ func wait_frames(frames, msg=''): _lgr.error(text) frames = 1 - var to_return = gut.set_wait_frames(frames, msg) - return to_return + _lgr.yield_msg(str('-- Awaiting ', frames, ' frame(s) -- ', msg)) + _awaiter.wait_frames(frames) + return _awaiter.timeout + +# p3 can be the optional message or an amount of time to wait between tests. +# p4 is the optional message if you have specified an amount of time to +# wait between tests. +func wait_until(callable, max_wait, p3='', p4=''): + var time_between = 0.0 + var message = p4 + if(typeof(p3) != TYPE_STRING): + time_between = p3 + else: + message = p3 + + _lgr.yield_msg(str("--Awaiting callable to return TRUE or ", max_wait, "s. ", message)) + _awaiter.wait_until(callable, max_wait, time_between) + await _awaiter.timeout + return !_awaiter.did_last_wait_timeout + + +func did_wait_timeout(): + return _awaiter.did_last_wait_timeout func yield_frames(frames, msg=''): _lgr.deprecated("yield_frames", "wait_frames") - var to_return = wait_frames(frames, msg) - return to_return + return wait_frames(frames, msg) + func get_summary(): return _summary @@ -1259,7 +1326,7 @@ func _smart_double(thing, double_strat, partial): else: to_return = gut.get_doubler().double_scene(thing, override_strat) - elif(_utils.is_native_class(thing)): + elif(GutUtils.is_native_class(thing)): if(partial): to_return = gut.get_doubler().partial_double_gdnative(thing) else: @@ -1286,7 +1353,7 @@ func _are_double_parameters_valid(thing, p2, p3): if(typeof(thing) == TYPE_STRING): bad_msg += "Doubling using the path to a script or scene is no longer supported. Load the script or scene and pass that to double instead.\n" - if(_utils.is_instance(thing)): + if(GutUtils.is_instance(thing)): bad_msg += "double requires a script, you passed an instance: " + _str(thing) if(bad_msg != ""): @@ -1381,27 +1448,36 @@ func ignore_method_when_doubling(thing, method_name): # Stub something. # # Parameters -# 1: the thing to stub, a file path or an instance or a class +# 1: A callable OR the thing to stub OR a file path OR an instance OR a Script # 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): +func stub(thing, p2=null, p3=null): var method_name = p2 var subpath = null + if(p3 != null): subpath = p2 method_name = p3 - if(_utils.is_instance(thing)): + if(GutUtils.is_instance(thing)): var msg = _get_bad_double_or_method_message(thing, method_name, 'stub') if(msg != ''): _lgr.error(msg) - return _utils.StubParams.new() + return GutUtils.StubParams.new() + + var sp = null + if(typeof(thing) == TYPE_CALLABLE): + if(p2 != null or p3 != null): + _lgr.error("Only one parameter expected when using a callable.") + sp = GutUtils.StubParams.new(thing) + else: + sp = GutUtils.StubParams.new(thing, method_name, subpath) - var sp = _utils.StubParams.new(thing, method_name, subpath) + sp.logger = _lgr gut.get_stubber().add_stub(sp) return sp @@ -1461,7 +1537,7 @@ func replace_node(base_node, path_or_node, with_this): func use_parameters(params): var ph = gut.parameter_handler if(ph == null): - ph = _utils.ParameterHandler.new(params) + ph = GutUtils.ParameterHandler.new(params) gut.parameter_handler = ph # DO NOT use gut.gd's get_call_count_text here since it decrements the @@ -1472,6 +1548,31 @@ func use_parameters(params): return ph.next_parameters() + +# ------------------------------------------------------------------------------ +# When used as the default for a test method parameter, it will cause the test +# to be run x times. +# +# I Hacked this together to test a method that was occassionally failing due to +# timing issues. I don't think it's a great idea, but you be the judge. +# ------------------------------------------------------------------------------ +func run_x_times(x): + var ph = gut.parameter_handler + if(ph == null): + _lgr.warn( + str("This test uses run_x_times and you really should not be ", + "using it. I don't think it's a good thing, but I did find it ", + "temporarily useful so I left it in here and didn't document it. ", + "Well, you found it, might as well open up an issue and let me ", + "know why you're doing this.")) + var params = [] + for i in range(x): + params.append(i) + + ph = GutUtils.ParameterHandler.new(params) + gut.parameter_handler = ph + return ph.next_parameters() + # ------------------------------------------------------------------------------ # Marks whatever is passed in to be freed after the test finishes. It also # returns what is passed in so you can save a line of code. @@ -1627,9 +1728,9 @@ func assert_not_same(v1, v2, text=''): # return # ------------------------------------------------------------------------------ func skip_if_godot_version_lt(expected): - var should_skip = !_utils.is_godot_version_gte(expected) + var should_skip = !GutUtils.is_godot_version_gte(expected) if(should_skip): - _pass(str('Skipping ', _utils.godot_version(), ' is less than ', expected)) + _pass(str('Skipping: ', GutUtils.godot_version_string(), ' is less than ', expected)) return should_skip @@ -1644,9 +1745,9 @@ func skip_if_godot_version_lt(expected): # return # ------------------------------------------------------------------------------ func skip_if_godot_version_ne(expected): - var should_skip = !_utils.is_godot_version(expected) + var should_skip = !GutUtils.is_godot_version(expected) if(should_skip): - _pass(str('Skipping ', _utils.godot_version(), ' is not ', expected)) + _pass(str('Skipping: ', GutUtils.godot_version_string(), ' is not ', expected)) return should_skip diff --git a/addons/gut/test_collector.gd b/addons/gut/test_collector.gd index 7e7337e9..b0047585 100644 --- a/addons/gut/test_collector.gd +++ b/addons/gut/test_collector.gd @@ -5,13 +5,13 @@ # # This also handles exporting and importing tests. # ------------------------------------------------------------------------------ -var CollectedScript = load('res://addons/gut/collected_script.gd') -var CollectedTest = load('res://addons/gut/collected_test.gd') +var CollectedScript = GutUtils.CollectedScript +var CollectedTest = GutUtils.CollectedTest var _test_prefix = 'test_' var _test_class_prefix = 'Test' -var _utils = load('res://addons/gut/utils.gd').get_instance() -var _lgr = _utils.get_logger() + +var _lgr = GutUtils.get_logger() # Array of CollectedScripts. @@ -52,7 +52,7 @@ func _get_inner_test_class_names(loaded): var const_map = loaded.get_script_constant_map() for key in const_map: var thing = const_map[key] - if(_utils.is_gdscript(thing)): + if(GutUtils.is_gdscript(thing)): if(key.begins_with(_test_class_prefix)): if(_does_inherit_from_test(thing)): inner_classes.append(key) @@ -72,7 +72,10 @@ func _parse_script(test_script): var inner_classes = [] var scripts_found = [] - var loaded = load(test_script.path) + var loaded = GutUtils.WarningsManager.load_script_using_custom_warnings( + test_script.path, + GutUtils.warnings_when_loading_test_scripts) + if(_does_inherit_from_test(loaded)): _populate_tests(test_script) scripts_found.append(test_script.path) @@ -83,7 +86,7 @@ func _parse_script(test_script): for i in range(inner_classes.size()): var loaded_inner = loaded.get(inner_classes[i]) if(_does_inherit_from_test(loaded_inner)): - var ts = CollectedScript.new(_utils, _lgr) + var ts = CollectedScript.new(_lgr) ts.path = test_script.path ts.inner_class_name = inner_classes[i] _populate_tests(ts) @@ -103,10 +106,16 @@ func add_script(path): # SHORTCIRCUIT if(!FileAccess.file_exists(path)): - _lgr.error('Could not find script: ' + path) - return + # This check was added so tests could create dynmaic scripts and add + # them to be run through gut. This helps cut down on creating test + # scripts to be used in test/resources. + if(ResourceLoader.has_cached(path)): + _lgr.debug("Using cached version of " + path) + else: + _lgr.error('Could not find script: ' + path) + return - var ts = CollectedScript.new(_utils, _lgr) + var ts = CollectedScript.new(_lgr) ts.path = path # Append right away because if we don't test_doubler.gd.TestInitParameters # will HARD crash. I couldn't figure out what was causing the issue but @@ -159,7 +168,7 @@ func import_tests(path): else: var sections = f.get_sections() for key in sections: - var ts = CollectedScript.new(_utils, _lgr) + var ts = CollectedScript.new(_lgr) ts.import_from(f, key) _populate_tests(ts) scripts.append(ts) @@ -168,7 +177,7 @@ func import_tests(path): func get_script_named(name): - return _utils.search_array(scripts, 'get_filename_and_inner', name) + return GutUtils.search_array(scripts, 'get_filename_and_inner', name) func get_test_named(script_name, test_name): diff --git a/addons/gut/thing_counter.gd b/addons/gut/thing_counter.gd index a9b0b489..40e71ef9 100644 --- a/addons/gut/thing_counter.gd +++ b/addons/gut/thing_counter.gd @@ -3,26 +3,36 @@ var things = {} func get_unique_count(): return things.size() + +func add_thing_to_count(thing): + if(!things.has(thing)): + things[thing] = 0 + + func add(thing): if(things.has(thing)): things[thing] += 1 else: things[thing] = 1 + func has(thing): return things.has(thing) -func get(thing): + +func count(thing): var to_return = 0 if(things.has(thing)): to_return = things[thing] return to_return + func sum(): - var count = 0 + var to_return = 0 for key in things: - count += things[key] - return count + to_return += things[key] + return to_return + func to_s(): var to_return = "" @@ -31,6 +41,7 @@ func to_s(): to_return += str("sum: ", sum()) return to_return + func get_max_count(): var max_val = null for key in things: @@ -38,6 +49,7 @@ func get_max_count(): max_val = things[key] return max_val + func add_array_items(array): for i in range(array.size()): add(array[i]) diff --git a/addons/gut/utils.gd b/addons/gut/utils.gd index df0c7b08..6580670e 100644 --- a/addons/gut/utils.gd +++ b/addons/gut/utils.gd @@ -1,18 +1,8 @@ @tool class_name GutUtils extends Object -# ------------------------------------------------------------------------------ -# Description -# ----------- -# This class is a PSUEDO SINGLETON. You should not make instances of it but use -# the get_instance static method. -# ------------------------------------------------------------------------------ -# NOTE: I think this can become completely static now that we have static -# variables. A lot would have to change though. But it would be good -# to do. -# ------------------------------------------------------------------------------ -const GUT_METADATA = '__gutdbl' +const GUT_METADATA = '__gutdbl' # Note, these cannot change since places are checking for TYPE_INT to determine # how to process parameters. @@ -21,7 +11,6 @@ enum DOUBLE_STRATEGY{ SCRIPT_ONLY, } - enum DIFF { DEEP, SIMPLE @@ -38,37 +27,213 @@ const TEST_STATUSES = { PASSED = 'pass' } +const DOUBLE_TEMPLATES = { + FUNCTION = 'res://addons/gut/double_templates/function_template.txt', + INIT = 'res://addons/gut/double_templates/init_template.txt', + SCRIPT = 'res://addons/gut/double_templates/script_template.txt', +} + + +static var GutScene = load('res://addons/gut/GutScene.tscn') +static var LazyLoader = load('res://addons/gut/lazy_loader.gd') +static var VersionNumbers = load("res://addons/gut/version_numbers.gd") +static var WarningsManager = load("res://addons/gut/warnings_manager.gd") +# -------------------------------- +# Lazy loaded scripts. These scripts are lazy loaded so that they can be +# declared, but will not load when this script is loaded. This gives us a +# window at the start of a run to adjust warning levels prior to loading +# everything. +# -------------------------------- +static var AutoFree = LazyLoader.new('res://addons/gut/autofree.gd'): + get: return AutoFree.get_loaded() + set(val): pass +static var Awaiter = LazyLoader.new('res://addons/gut/awaiter.gd'): + get: return Awaiter.get_loaded() + set(val): pass +static var Comparator = LazyLoader.new('res://addons/gut/comparator.gd'): + get: return Comparator.get_loaded() + set(val): pass +static var CollectedTest = LazyLoader.new('res://addons/gut/collected_test.gd'): + get: return CollectedTest.get_loaded() + set(val): pass +static var CollectedScript = LazyLoader.new('res://addons/gut/collected_script.gd'): + get: return CollectedScript.get_loaded() + set(val): pass +static var CompareResult = LazyLoader.new('res://addons/gut/compare_result.gd'): + get: return CompareResult.get_loaded() + set(val): pass +static var DiffTool = LazyLoader.new('res://addons/gut/diff_tool.gd'): + get: return DiffTool.get_loaded() + set(val): pass +static var Doubler = LazyLoader.new('res://addons/gut/doubler.gd'): + get: return Doubler.get_loaded() + set(val): pass +static var DynamicGdScript = LazyLoader.new("res://addons/gut/dynamic_gdscript.gd") : + get: return DynamicGdScript.get_loaded() + set(val): pass +static var Gut = LazyLoader.new('res://addons/gut/gut.gd'): + get: return Gut.get_loaded() + set(val): pass +static var GutConfig = LazyLoader.new('res://addons/gut/gut_config.gd'): + get: return GutConfig.get_loaded() + set(val): pass +static var HookScript = LazyLoader.new('res://addons/gut/hook_script.gd'): + get: return HookScript.get_loaded() + set(val): pass +static var InnerClassRegistry = LazyLoader.new('res://addons/gut/inner_class_registry.gd'): + get: return InnerClassRegistry.get_loaded() + set(val): pass +static var InputFactory = LazyLoader.new("res://addons/gut/input_factory.gd"): + get: return InputFactory.get_loaded() + set(val): pass +static var InputSender = LazyLoader.new("res://addons/gut/input_sender.gd"): + get: return InputSender.get_loaded() + set(val): pass +static var JunitXmlExport = LazyLoader.new('res://addons/gut/junit_xml_export.gd'): + get: return JunitXmlExport.get_loaded() + set(val): pass +static var Logger = LazyLoader.new('res://addons/gut/logger.gd') : # everything should use get_logger + get: return Logger.get_loaded() + set(val): pass +static var MethodMaker = LazyLoader.new('res://addons/gut/method_maker.gd'): + get: return MethodMaker.get_loaded() + set(val): pass +static var OneToMany = LazyLoader.new('res://addons/gut/one_to_many.gd'): + get: return OneToMany.get_loaded() + set(val): pass +static var OrphanCounter = LazyLoader.new('res://addons/gut/orphan_counter.gd'): + get: return OrphanCounter.get_loaded() + set(val): pass +static var ParameterFactory = LazyLoader.new('res://addons/gut/parameter_factory.gd'): + get: return ParameterFactory.get_loaded() + set(val): pass +static var ParameterHandler = LazyLoader.new('res://addons/gut/parameter_handler.gd'): + get: return ParameterHandler.get_loaded() + set(val): pass +static var Printers = LazyLoader.new('res://addons/gut/printers.gd'): + get: return Printers.get_loaded() + set(val): pass +static var ResultExporter = LazyLoader.new('res://addons/gut/result_exporter.gd'): + get: return ResultExporter.get_loaded() + set(val): pass +static var ScriptCollector = LazyLoader.new('res://addons/gut/script_parser.gd'): + get: return ScriptCollector.get_loaded() + set(val): pass +static var Spy = LazyLoader.new('res://addons/gut/spy.gd'): + get: return Spy.get_loaded() + set(val): pass +static var Strutils = LazyLoader.new('res://addons/gut/strutils.gd'): + get: return Strutils.get_loaded() + set(val): pass +static var Stubber = LazyLoader.new('res://addons/gut/stubber.gd'): + get: return Stubber.get_loaded() + set(val): pass +static var StubParams = LazyLoader.new('res://addons/gut/stub_params.gd'): + get: return StubParams.get_loaded() + set(val): pass +static var Summary = LazyLoader.new('res://addons/gut/summary.gd'): + get: return Summary.get_loaded() + set(val): pass +static var Test = LazyLoader.new('res://addons/gut/test.gd'): + get: return Test.get_loaded() + set(val): pass +static var TestCollector = LazyLoader.new('res://addons/gut/test_collector.gd'): + get: return TestCollector.get_loaded() + set(val): pass +static var ThingCounter = LazyLoader.new('res://addons/gut/thing_counter.gd'): + get: return ThingCounter.get_loaded() + set(val): pass +# -------------------------------- + static var avail_fonts = ['AnonymousPro', 'CourierPrime', 'LobsterTwo', 'Default'] +static var version_numbers = VersionNumbers.new( + # gut_versrion (source of truth) + '9.3.0', + # required_godot_version + '4.2.0' +) + + +static var warnings_at_start := { # WarningsManager dictionary + exclude_addons = true +} -# This is a holdover from when GUT was making a psuedo autoload. It would add -# an instance of this class to the tree with a name and retrieve it when -# get_instance was called. We now have static variables so this var is now -# used instead of a node. -static var _the_instance = null +static var warnings_when_loading_test_scripts := { # WarningsManager dictionary + enable = false +} # ------------------------------------------------------------------------------ -# Gets the root node without having to be in the tree and pushing out an error -# if we don't have a main loop ready to go yet. +# Everything should get a logger through this. +# +# When running in test mode this will always return a new logger so that errors +# are not caused by getting bad warn/error/etc counts. # ------------------------------------------------------------------------------ -static func get_root_node(): - var main_loop = Engine.get_main_loop() - if(main_loop != null): - return main_loop.root +static var _test_mode = false +static var _lgr = null +static func get_logger(): + if(_test_mode): + return Logger.new() else: - push_error('No Main Loop Yet') - return null + if(_lgr == null): + _lgr = Logger.new() + return _lgr + + +static var _dyn_gdscript = DynamicGdScript.new() +static func create_script_from_source(source, override_path=null): + var DynamicScript = _dyn_gdscript.create_script_from_source(source, override_path) + + if(typeof(DynamicScript) == TYPE_INT): + var l = get_logger() + l.error(str('Could not create script from source. Error: ', DynamicScript)) + l.info(str("Source Code:\n", add_line_numbers(source))) + + return DynamicScript + + +static func godot_version_string(): + return version_numbers.make_godot_version_string() + + +static func is_godot_version(expected): + return VersionNumbers.VerNumTools.is_godot_version_eq(expected) + + +static func is_godot_version_gte(expected): + return VersionNumbers.VerNumTools.is_godot_version_gte(expected) + + +const INSTALL_OK_TEXT = 'Everything checks out' +static func make_install_check_text(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): + var text = INSTALL_OK_TEXT + if(!FileAccess.file_exists(template_paths.FUNCTION) or + !FileAccess.file_exists(template_paths.INIT) or + !FileAccess.file_exists(template_paths.SCRIPT)): + + text = 'One or more GUT template files are missing. If this is an exported project, you must include *.txt files in the export to run GUT. If it is not an exported project then reinstall GUT.' + elif(!ver_nums.is_godot_version_valid()): + text = ver_nums.get_bad_version_text() + + return text + + +static func is_install_valid(template_paths=DOUBLE_TEMPLATES, ver_nums=version_numbers): + return make_install_check_text(template_paths, ver_nums) == INSTALL_OK_TEXT # ------------------------------------------------------------------------------ -# Get the ONE instance of utils +# Gets the root node without having to be in the tree and pushing out an error +# if we don't have a main loop ready to go yet. # ------------------------------------------------------------------------------ -static func get_instance(): - if(_the_instance == null): - _the_instance = GutUtils.new() - - return _the_instance +# static func get_root_node(): +# var main_loop = Engine.get_main_loop() +# if(main_loop != null): +# return main_loop.root +# else: +# push_error('No Main Loop Yet') +# return null # ------------------------------------------------------------------------------ @@ -119,8 +284,8 @@ static func nvl(value, if_null): # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ -static func pretty_print(dict): - print(JSON.stringify(dict, ' ')) +static func pretty_print(dict, indent = ' '): + print(JSON.stringify(dict, indent)) # ------------------------------------------------------------------------------ @@ -164,136 +329,6 @@ static func get_scene_script_object(scene): return to_return -# ############################################################################## -# Start Class -# ############################################################################## -var Logger = load('res://addons/gut/logger.gd') # everything should use get_logger -var _lgr = null -var json = JSON.new() - -var _test_mode = false - -var AutoFree = load('res://addons/gut/autofree.gd') -var Awaiter = load('res://addons/gut/awaiter.gd') -var Comparator = load('res://addons/gut/comparator.gd') -var CompareResult = load('res://addons/gut/compare_result.gd') -var DiffTool = load('res://addons/gut/diff_tool.gd') -var Doubler = load('res://addons/gut/doubler.gd') -var Gut = load('res://addons/gut/gut.gd') -var GutConfig = load('res://addons/gut/gut_config.gd') -var HookScript = load('res://addons/gut/hook_script.gd') -var InnerClassRegistry = load('res://addons/gut/inner_class_registry.gd') -var InputFactory = load("res://addons/gut/input_factory.gd") -var InputSender = load("res://addons/gut/input_sender.gd") -var JunitXmlExport = load('res://addons/gut/junit_xml_export.gd') -var MethodMaker = load('res://addons/gut/method_maker.gd') -var OneToMany = load('res://addons/gut/one_to_many.gd') -var OrphanCounter = load('res://addons/gut/orphan_counter.gd') -var ParameterFactory = load('res://addons/gut/parameter_factory.gd') -var ParameterHandler = load('res://addons/gut/parameter_handler.gd') -var Printers = load('res://addons/gut/printers.gd') -var ResultExporter = load('res://addons/gut/result_exporter.gd') -var ScriptCollector = load('res://addons/gut/script_parser.gd') -var Spy = load('res://addons/gut/spy.gd') -var Strutils = load('res://addons/gut/strutils.gd') -var Stubber = load('res://addons/gut/stubber.gd') -var StubParams = load('res://addons/gut/stub_params.gd') -var Summary = load('res://addons/gut/summary.gd') -var Test = load('res://addons/gut/test.gd') -var TestCollector = load('res://addons/gut/test_collector.gd') -var ThingCounter = load('res://addons/gut/thing_counter.gd') -var CollectedTest = load('res://addons/gut/collected_test.gd') -var CollectedScript = load('res://addons/gut/collected_test.gd') - -var GutScene = load('res://addons/gut/GutScene.tscn') - -# Source of truth for the GUT version -var version = '9.2.1' -# The required Godot version as an array. -var req_godot = [4, 2, 0] - - -# ------------------------------------------------------------------------------ -# Blurb of text with GUT and Godot versions. -# ------------------------------------------------------------------------------ -func get_version_text(): - var v_info = Engine.get_version_info() - var gut_version_info = str('GUT version: ', version) - var godot_version_info = str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch) - return godot_version_info + "\n" + gut_version_info - - -# ------------------------------------------------------------------------------ -# Returns a nice string for erroring out when we have a bad Godot version. -# ------------------------------------------------------------------------------ -func get_bad_version_text(): - var ver = '.'.join(PackedStringArray(req_godot)) - var info = Engine.get_version_info() - var gd_version = str(info.major, '.', info.minor, '.', info.patch) - return 'GUT ' + version + ' requires Godot ' + ver + ' or greater. Godot version is ' + gd_version - - -# ------------------------------------------------------------------------------ -# Checks the Godot version against req_godot array. -# ------------------------------------------------------------------------------ -func is_version_ok(engine_info=Engine.get_version_info(),required=req_godot): - var is_ok = null - var engine_array = [engine_info.major, engine_info.minor, engine_info.patch] - - var idx = 0 - while(is_ok == null and idx < engine_array.size()): - if(engine_array[idx] > required[idx]): - is_ok = true - elif(engine_array[idx] < required[idx]): - is_ok = false - - idx += 1 - - # still null means each index was the same. - return nvl(is_ok, true) - - -func godot_version(engine_info=Engine.get_version_info()): - return str(engine_info.major, '.', engine_info.minor, '.', engine_info.patch) - - -func is_godot_version(expected, engine_info=Engine.get_version_info()): - var engine_array = [engine_info.major, engine_info.minor, engine_info.patch] - var expected_array = expected.split('.') - - if(expected_array.size() > engine_array.size()): - return false - - var is_version = true - var i = 0 - while(i < expected_array.size() and i < engine_array.size() and is_version): - if(expected_array[i] == str(engine_array[i])): - i += 1 - else: - is_version = false - - return is_version - - -func is_godot_version_gte(expected, engine_info=Engine.get_version_info()): - return is_version_ok(engine_info, expected.split('.')) - - -# ------------------------------------------------------------------------------ -# Everything should get a logger through this. -# -# When running in test mode this will always return a new logger so that errors -# are not caused by getting bad warn/error/etc counts. -# ------------------------------------------------------------------------------ -func get_logger(): - if(_test_mode): - return Logger.new() - else: - if(_lgr == null): - _lgr = Logger.new() - return _lgr - - # ------------------------------------------------------------------------------ # returns true if the object has been freed, false if not # @@ -301,7 +336,7 @@ func get_logger(): # of the time but sometimes it does not catch it. The str comparison seems to # fill in the gaps. I've not seen any errors after adding that check. # ------------------------------------------------------------------------------ -func is_freed(obj): +static func is_freed(obj): var wr = weakref(obj) return !(wr.get_ref() and str(obj) != '') @@ -309,30 +344,44 @@ func is_freed(obj): # ------------------------------------------------------------------------------ # Pretty self explanitory. # ------------------------------------------------------------------------------ -func is_not_freed(obj): +static func is_not_freed(obj): return !is_freed(obj) # ------------------------------------------------------------------------------ # Checks if the passed in object is a GUT Double or Partial Double. # ------------------------------------------------------------------------------ -func is_double(obj): +static func is_double(obj): var to_return = false if(typeof(obj) == TYPE_OBJECT and is_instance_valid(obj)): to_return = obj.has_method('__gutdbl_check_method__') return to_return +# ------------------------------------------------------------------------------ +# Checks an object to see if it is a GDScriptNativeClass +# ------------------------------------------------------------------------------ +static func is_native_class(thing): + var it_is = false + if(typeof(thing) == TYPE_OBJECT): + it_is = str(thing).begins_with(" r[idx]): + is_ok = true + elif(v[idx] < r[idx]): + is_ok = false + + idx += 1 + + # still null means each index was the same. + return GutUtils.nvl(is_ok, true) + + + static func is_version_eq(version, expected): + var version_array = make_version_array(version) + var expected_array = make_version_array(expected) + + if(expected_array.size() > version_array.size()): + return false + + var is_version = true + var i = 0 + while(i < expected_array.size() and i < version_array.size() and is_version): + if(expected_array[i] == version_array[i]): + i += 1 + else: + is_version = false + + return is_version + + + static func is_godot_version_eq(expected): + return VerNumTools.is_version_eq(Engine.get_version_info(), expected) + + + static func is_godot_version_gte(expected): + return VerNumTools.is_version_gte(Engine.get_version_info(), expected) + + + + +# ############################################################################## +# +# ############################################################################## +var gut_version = '0.0.0' +var required_godot_version = '0.0.0' + +func _init(gut_v = gut_version, required_godot_v = required_godot_version): + gut_version = gut_v + required_godot_version = required_godot_v + + +# ------------------------------------------------------------------------------ +# Blurb of text with GUT and Godot versions. +# ------------------------------------------------------------------------------ +func get_version_text(): + var v_info = Engine.get_version_info() + var gut_version_info = str('GUT version: ', gut_version) + var godot_version_info = str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch) + return godot_version_info + "\n" + gut_version_info + + +# ------------------------------------------------------------------------------ +# Returns a nice string for erroring out when we have a bad Godot version. +# ------------------------------------------------------------------------------ +func get_bad_version_text(): + var info = Engine.get_version_info() + var gd_version = str(info.major, '.', info.minor, '.', info.patch) + return 'GUT ' + gut_version + ' requires Godot ' + required_godot_version + \ + ' or greater. Godot version is ' + gd_version + + +# ------------------------------------------------------------------------------ +# Checks the Godot version against required_godot_version. +# ------------------------------------------------------------------------------ +func is_godot_version_valid(): + return VerNumTools.is_version_gte(Engine.get_version_info(), required_godot_version) + + +func make_godot_version_string(): + return VerNumTools.make_version_string(Engine.get_version_info()) diff --git a/addons/gut/warnings_manager.gd b/addons/gut/warnings_manager.gd new file mode 100644 index 00000000..bfc40461 --- /dev/null +++ b/addons/gut/warnings_manager.gd @@ -0,0 +1,137 @@ +const IGNORE = 0 +const WARN = 1 +const ERROR = 2 + + +const WARNING_LOOKUP = { + IGNORE : 'IGNORE', + WARN : 'WARN', + ERROR : 'ERROR' +} + +const GDSCRIPT_WARNING = 'debug/gdscript/warnings/' + +# --------------------------------------- +# Static +# --------------------------------------- +static var _static_init_called = false +# This is static and set in _static_init so that we can get the current settings as +# soon as possible. +static var _project_warnings : Dictionary = {} +static var project_warnings := {} : + get: + # somehow this gets called before _project_warnings is initialized when + # loading a project in the editor. It causes an error stating that + # duplicate can't be called on nil. It seems there might be an + # implicit "get" call happening. Using push_error I saw a message + # in this method, but not one from _static_init upon loading the project + if(_static_init_called): + return _project_warnings.duplicate() + else: + return {} + set(val): pass + + +static func _static_init(): + _project_warnings = create_warnings_dictionary_from_project_settings() + _static_init_called = true + + +static func are_warnings_enabled(): + return ProjectSettings.get(str(GDSCRIPT_WARNING, 'enable')) + + +## Turn all warnings on/off. Use reset_warnings to restore the original value. +static func enable_warnings(should=true): + ProjectSettings.set(str(GDSCRIPT_WARNING, 'enable'), should) + + +## Turn on/off excluding addons. Use reset_warnings to restore the original value. +static func exclude_addons(should=true): + ProjectSettings.set(str(GDSCRIPT_WARNING, 'exclude_addons'), should) + + +## Resets warning settings to what they are set to in Project Settings +static func reset_warnings(): + apply_warnings_dictionary(_project_warnings) + + + +static func set_project_setting_warning(warning_name : String, value : Variant): + var property_name = str(GDSCRIPT_WARNING, warning_name) + # This check will generate a warning if the setting does not exist + if(property_name in ProjectSettings): + ProjectSettings.set(property_name, value) + + +static func apply_warnings_dictionary(warning_values : Dictionary): + for key in warning_values: + set_project_setting_warning(key, warning_values[key]) + +# --------------------------------------- +# Class +# --------------------------------------- +static func create_ignore_all_dictionary(): + return replace_warnings_values(project_warnings, -1, IGNORE) + + +static func create_warn_all_warnings_dictionary(): + return replace_warnings_values(project_warnings, -1, WARN) + + +static func replace_warnings_with_ignore(dict): + return replace_warnings_values(dict, WARN, IGNORE) + + +static func replace_errors_with_warnings(dict): + return replace_warnings_values(dict, ERROR, WARN) + + +static func replace_warnings_values(dict, replace_this, with_this): + var to_return = dict.duplicate() + for key in to_return: + if(typeof(to_return[key]) == TYPE_INT and (replace_this == -1 or to_return[key] == replace_this)): + to_return[key] = with_this + return to_return + + +static func create_warnings_dictionary_from_project_settings() -> Dictionary : + var props = ProjectSettings.get_property_list() + var to_return = {} + for i in props.size(): + if(props[i].name.begins_with(GDSCRIPT_WARNING)): + var prop_name = props[i].name.replace(GDSCRIPT_WARNING, '') + to_return[prop_name] = ProjectSettings.get(props[i].name) + return to_return + + +static func print_warnings_dictionary(which : Dictionary): + var is_valid = true + for key in which: + var value_str = str(which[key]) + if(_project_warnings.has(key)): + if(typeof(which[key]) == TYPE_INT): + if(WARNING_LOOKUP.has(which[key])): + value_str = WARNING_LOOKUP[which[key]] + else: + push_warning(str(which[key], ' is not a valid value for ', key)) + is_valid = false + else: + push_warning(str(key, ' is not a valid warning setting')) + is_valid = false + var s = str(key, ' = ', value_str) + print(s) + return is_valid + + +static func load_script_ignoring_all_warnings(path : String) -> Variant: + return load_script_using_custom_warnings(path, create_ignore_all_dictionary()) + + +static func load_script_using_custom_warnings(path : String, warnings_dictionary : Dictionary) -> Variant: + var current_warns = create_warnings_dictionary_from_project_settings() + apply_warnings_dictionary(warnings_dictionary) + var s = load(path) + apply_warnings_dictionary(current_warns) + + return s