first enemy test
This commit is contained in:
239
addons/gut/cli/change_project_warnings.gd
Normal file
239
addons/gut/cli/change_project_warnings.gd
Normal file
@ -0,0 +1,239 @@
|
||||
extends SceneTree
|
||||
|
||||
var Optparse = load('res://addons/gut/cli/optparse.gd')
|
||||
var WarningsManager = load("res://addons/gut/warnings_manager.gd")
|
||||
const WARN_VALUE_PRINT_POSITION = 36
|
||||
|
||||
var godot_default_warnings = {
|
||||
"assert_always_false": 1, "assert_always_true": 1, "confusable_identifier": 1,
|
||||
"confusable_local_declaration": 1, "confusable_local_usage": 1, "constant_used_as_function": 1,
|
||||
"deprecated_keyword": 1, "empty_file": 1, "enable": true,
|
||||
"exclude_addons": true, "function_used_as_property": 1, "get_node_default_without_onready": 2,
|
||||
"incompatible_ternary": 1, "inference_on_variant": 2, "inferred_declaration": 0,
|
||||
"int_as_enum_without_cast": 1, "int_as_enum_without_match": 1, "integer_division": 1,
|
||||
"narrowing_conversion": 1, "native_method_override": 2, "onready_with_export": 2,
|
||||
"property_used_as_function": 1, "redundant_await": 1, "redundant_static_unload": 1,
|
||||
"renamed_in_godot_4_hint": 1, "return_value_discarded": 0, "shadowed_global_identifier": 1,
|
||||
"shadowed_variable": 1, "shadowed_variable_base_class": 1, "standalone_expression": 1,
|
||||
"standalone_ternary": 1, "static_called_on_instance": 1, "unassigned_variable": 1,
|
||||
"unassigned_variable_op_assign": 1, "unreachable_code": 1, "unreachable_pattern": 1,
|
||||
"unsafe_call_argument": 0, "unsafe_cast": 0, "unsafe_method_access": 0,
|
||||
"unsafe_property_access": 0, "unsafe_void_return": 1, "untyped_declaration": 0,
|
||||
"unused_local_constant": 1, "unused_parameter": 1, "unused_private_class_variable": 1,
|
||||
"unused_signal": 1, "unused_variable": 1
|
||||
}
|
||||
|
||||
var gut_default_changes = {
|
||||
"exclude_addons": false, "redundant_await": 0,
|
||||
}
|
||||
|
||||
var warning_settings = {}
|
||||
|
||||
func _setup_warning_settings():
|
||||
warning_settings["godot_default"] = godot_default_warnings
|
||||
warning_settings["current"] = WarningsManager.create_warnings_dictionary_from_project_settings()
|
||||
warning_settings["all_warn"] = WarningsManager.create_warn_all_warnings_dictionary()
|
||||
|
||||
var gut_default = godot_default_warnings.duplicate()
|
||||
gut_default.merge(gut_default_changes, true)
|
||||
warning_settings["gut_default"] = gut_default
|
||||
|
||||
|
||||
func _warn_value_to_s(value):
|
||||
var readable = str(value).capitalize()
|
||||
if(typeof(value) == TYPE_INT):
|
||||
readable = WarningsManager.WARNING_LOOKUP.get(value, str(readable, ' ???'))
|
||||
readable = readable.capitalize()
|
||||
return readable
|
||||
|
||||
|
||||
func _human_readable(warnings):
|
||||
var to_return = ""
|
||||
for key in warnings:
|
||||
var readable = _warn_value_to_s(warnings[key])
|
||||
to_return += str(key.capitalize().rpad(35, ' '), readable, "\n")
|
||||
return to_return
|
||||
|
||||
|
||||
func _dump_settings(which):
|
||||
if(warning_settings.has(which)):
|
||||
GutUtils.pretty_print(warning_settings[which])
|
||||
else:
|
||||
print("UNKNOWN print option ", which)
|
||||
|
||||
|
||||
func _print_settings(which):
|
||||
if(warning_settings.has(which)):
|
||||
print(_human_readable(warning_settings[which]))
|
||||
else:
|
||||
print("UNKNOWN print option ", which)
|
||||
|
||||
|
||||
func _apply_settings(which):
|
||||
if(!warning_settings.has(which)):
|
||||
print("UNKNOWN set option ", which)
|
||||
return
|
||||
|
||||
var pre_settings = warning_settings["current"]
|
||||
var new_settings = warning_settings[which]
|
||||
|
||||
if(new_settings == pre_settings):
|
||||
print("-- Settings are the same, no changes were made --")
|
||||
return
|
||||
|
||||
WarningsManager.apply_warnings_dictionary(new_settings)
|
||||
ProjectSettings.save()
|
||||
print("-- Project Warning Settings have been updated --")
|
||||
print(_diff_changes_text(pre_settings))
|
||||
|
||||
|
||||
func _diff_text(w1, w2, diff_col_pad=10):
|
||||
var to_return = ""
|
||||
for key in w1:
|
||||
var v1_text = _warn_value_to_s(w1[key])
|
||||
var v2_text = _warn_value_to_s(w2[key])
|
||||
var diff_text = v1_text
|
||||
var prefix = " "
|
||||
|
||||
if(v1_text != v2_text):
|
||||
var diff_prefix = " "
|
||||
if(w1[key] > w2[key]):
|
||||
diff_prefix = "-"
|
||||
else:
|
||||
diff_prefix = "+"
|
||||
prefix = "* "
|
||||
diff_text = str(v1_text.rpad(diff_col_pad, ' '), diff_prefix, v2_text)
|
||||
|
||||
to_return += str(str(prefix, key.capitalize()).rpad(WARN_VALUE_PRINT_POSITION, ' '), diff_text, "\n")
|
||||
|
||||
return to_return.rstrip("\n")
|
||||
|
||||
|
||||
func _diff_changes_text(pre_settings):
|
||||
var orig_diff_text = _diff_text(
|
||||
pre_settings,
|
||||
WarningsManager.create_warnings_dictionary_from_project_settings(),
|
||||
0)
|
||||
# these next two lines are fragile and brute force...enjoy
|
||||
var diff_text = orig_diff_text.replace("-", " -> ")
|
||||
diff_text = diff_text.replace("+", " -> ")
|
||||
|
||||
if(orig_diff_text == diff_text):
|
||||
diff_text += "\n-- No changes were made --"
|
||||
else:
|
||||
diff_text += "\nChanges will not be visible in Godot until it is restarted.\n"
|
||||
diff_text += "Even if it asks you to reload...Maybe. Probably."
|
||||
|
||||
return diff_text
|
||||
|
||||
|
||||
|
||||
func _diff(name_1, name_2):
|
||||
if(warning_settings.has(name_1) and warning_settings.has(name_2)):
|
||||
var c2_pad = name_1.length() + 2
|
||||
var heading = str(" ".repeat(WARN_VALUE_PRINT_POSITION), name_1.rpad(c2_pad, ' '), name_2, "\n")
|
||||
heading += str(
|
||||
" ".repeat(WARN_VALUE_PRINT_POSITION),
|
||||
"-".repeat(name_1.length()).rpad(c2_pad, " "),
|
||||
"-".repeat(name_2.length()),
|
||||
"\n")
|
||||
|
||||
var text = _diff_text(warning_settings[name_1], warning_settings[name_2], c2_pad)
|
||||
|
||||
print(heading)
|
||||
print(text)
|
||||
|
||||
var diff_count = 0
|
||||
for line in text.split("\n"):
|
||||
if(!line.begins_with(" ")):
|
||||
diff_count += 1
|
||||
|
||||
if(diff_count == 0):
|
||||
print('-- [', name_1, "] and [", name_2, "] are the same --")
|
||||
else:
|
||||
print('-- There are ', diff_count, ' differences between [', name_1, "] and [", name_2, "] --")
|
||||
else:
|
||||
print("One or more unknown Warning Level Names:, [", name_1, "] [", name_2, "]")
|
||||
|
||||
|
||||
func _set_settings(nvps):
|
||||
var pre_settings = warning_settings["current"]
|
||||
for i in range(nvps.size()/2):
|
||||
var s_name = nvps[i * 2]
|
||||
var s_value = nvps[i * 2 + 1]
|
||||
if(godot_default_warnings.has(s_name)):
|
||||
var t = typeof(godot_default_warnings[s_name])
|
||||
if(t == TYPE_INT):
|
||||
s_value = s_value.to_int()
|
||||
elif(t == TYPE_BOOL):
|
||||
s_value = s_value.to_lower() == 'true'
|
||||
|
||||
WarningsManager.set_project_setting_warning(s_name, s_value)
|
||||
ProjectSettings.save()
|
||||
print(_diff_changes_text(pre_settings))
|
||||
|
||||
|
||||
|
||||
func _setup_options():
|
||||
var opts = Optparse.new()
|
||||
opts.banner = """
|
||||
This script prints info about or sets the warning settings for the project.
|
||||
Each action requires one or more Warning Level Names.
|
||||
|
||||
Warning Level Names:
|
||||
* current The current settings for the project.
|
||||
* godot_default The default settings for Godot.
|
||||
* gut_default The warning settings that is used when developing GUT.
|
||||
* all_warn Everything set to warn.
|
||||
""".dedent()
|
||||
|
||||
opts.add('-h', false, 'Print this help')
|
||||
opts.add('-set', [], "Sets a single setting in the project settings and saves.\n" +
|
||||
"Use -dump to see a list of setting names and values.\n" +
|
||||
"Example: -set enabled,true -set unsafe_cast,2 -set unreachable_code,0")
|
||||
opts.add_heading(" Actions (require Warning Level Name)")
|
||||
opts.add('-diff', [], "Shows the difference between two Warning Level Names.\n" +
|
||||
"Example: -diff current,all_warn")
|
||||
opts.add('-dump', 'none', "Prints a dictionary of the warning values.")
|
||||
opts.add('-print', 'none', "Print human readable warning values.")
|
||||
opts.add('-apply', 'none', "Applys one of the Warning Level Names to the project settings. You should restart after using this")
|
||||
|
||||
return opts
|
||||
|
||||
func _print_help(opts):
|
||||
opts.print_help()
|
||||
|
||||
|
||||
|
||||
func _init():
|
||||
# Testing might set this flag but it should never be disabled for this tool
|
||||
# or it cannot save project settings, but says it did. Sneakily use the
|
||||
# private property to get around this property being read-only. Don't
|
||||
# try this at home.
|
||||
WarningsManager._disabled = false
|
||||
|
||||
_setup_warning_settings()
|
||||
|
||||
var opts = _setup_options()
|
||||
opts.parse()
|
||||
|
||||
if(opts.unused.size() != 0):
|
||||
opts.print_help()
|
||||
print("Unknown arguments ", opts.unused)
|
||||
if(opts.values.h):
|
||||
opts.print_help()
|
||||
elif(opts.values.print != 'none'):
|
||||
_print_settings(opts.values.print)
|
||||
elif(opts.values.dump != 'none'):
|
||||
_dump_settings(opts.values.dump)
|
||||
elif(opts.values.apply != 'none'):
|
||||
_apply_settings(opts.values.apply )
|
||||
elif(opts.values.diff.size() == 2):
|
||||
_diff(opts.values.diff[0], opts.values.diff[1])
|
||||
elif(opts.values.set.size() % 2 == 0):
|
||||
_set_settings(opts.values.set)
|
||||
else:
|
||||
opts.print_help()
|
||||
print("You didn't specify any options or too many or not the right size or something invalid. I don't know what you want to do.")
|
||||
|
||||
quit()
|
1
addons/gut/cli/change_project_warnings.gd.uid
Normal file
1
addons/gut/cli/change_project_warnings.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cqcxu3fduawal
|
297
addons/gut/cli/gut_cli.gd
Normal file
297
addons/gut/cli/gut_cli.gd
Normal file
@ -0,0 +1,297 @@
|
||||
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.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) 2025 Tom "Butch" Wesley
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# ##############################################################################
|
1
addons/gut/cli/gut_cli.gd.uid
Normal file
1
addons/gut/cli/gut_cli.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://co6j335bietg1
|
530
addons/gut/cli/optparse.gd
Normal file
530
addons/gut/cli/optparse.gd
Normal file
@ -0,0 +1,530 @@
|
||||
## Parses command line arguments, as one might expect.
|
||||
##
|
||||
## Parses command line arguments with a bunch of options including generating
|
||||
## text that displays all the arguments your script accepts. This
|
||||
## is included in the GUT ClassRef since it might be usable by others and is
|
||||
## portable (everything it needs is in this one file).
|
||||
## [br]
|
||||
## This does alot, if you want to see it in action have a look at
|
||||
## [url=https://github.com/bitwes/Gut/blob/main/scratch/optparse_example.gd]scratch/optparse_example.gd[/url]
|
||||
## [codeblock lang=text]
|
||||
##
|
||||
## 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.
|
||||
## [/codeblock]
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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 line_indent = str("\n", " ".repeat(min_space + 1))
|
||||
var subbed_desc = description
|
||||
subbed_desc = subbed_desc.replace('[default]', str(default))
|
||||
subbed_desc = subbed_desc.replace("\n", line_indent)
|
||||
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).replace("\n", "\n "), "\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 "<path to godot> -s " + script_option.value + " [opts] " + pos_text
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# optarse
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
var options = Options.new()
|
||||
var banner = ''
|
||||
var option_name_prefix = '-'
|
||||
var unused = []
|
||||
var parsed_args = []
|
||||
var values = {}
|
||||
|
||||
func _populate_values_dictionary():
|
||||
for entry in options.options:
|
||||
var value_key = entry.option_name.lstrip('-')
|
||||
values[value_key] = entry.value
|
||||
|
||||
for entry in options.positional:
|
||||
var value_key = entry.option_name.lstrip('-')
|
||||
values[value_key] = entry.value
|
||||
|
||||
|
||||
|
||||
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 str(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)
|
||||
_populate_values_dictionary()
|
||||
|
||||
|
||||
func get_missing_required_options():
|
||||
return options.get_missing_required_options()
|
||||
|
||||
|
||||
# ##############################################################################
|
||||
# The MIT License (MIT)
|
||||
# =====================
|
||||
#
|
||||
# Copyright (c) 2025 Tom "Butch" Wesley
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# ##############################################################################
|
1
addons/gut/cli/optparse.gd.uid
Normal file
1
addons/gut/cli/optparse.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bkqmqfhwouydo
|
Reference in New Issue
Block a user