making sure the issue comes from GDUnit addon folder
All checks were successful
Create tag and build when new code gets to main / Export (push) Successful in 7m6s
All checks were successful
Create tag and build when new code gets to main / Export (push) Successful in 7m6s
This commit is contained in:
0
addons/gdUnit4/src/core/GdArrayTools.gd
Normal file
0
addons/gdUnit4/src/core/GdArrayTools.gd
Normal file
0
addons/gdUnit4/src/core/GdArrayTools.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdArrayTools.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdDiffTool.gd
Normal file
0
addons/gdUnit4/src/core/GdDiffTool.gd
Normal file
0
addons/gdUnit4/src/core/GdDiffTool.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdDiffTool.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdObjects.gd
Normal file
0
addons/gdUnit4/src/core/GdObjects.gd
Normal file
0
addons/gdUnit4/src/core/GdObjects.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdObjects.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnit4Version.gd
Normal file
0
addons/gdUnit4/src/core/GdUnit4Version.gd
Normal file
0
addons/gdUnit4/src/core/GdUnit4Version.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnit4Version.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitFileAccess.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitFileAccess.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid
Normal file
81
addons/gdUnit4/src/core/GdUnitProperty.gd
Normal file
81
addons/gdUnit4/src/core/GdUnitProperty.gd
Normal file
@@ -0,0 +1,81 @@
|
||||
class_name GdUnitProperty
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var _name :String
|
||||
var _help :String
|
||||
var _type :int
|
||||
var _value :Variant
|
||||
var _value_set :PackedStringArray
|
||||
var _default :Variant
|
||||
|
||||
|
||||
func _init(p_name :String, p_type :int, p_value :Variant, p_default_value :Variant, p_help :="", p_value_set := PackedStringArray()) -> void:
|
||||
_name = p_name
|
||||
_type = p_type
|
||||
_value = p_value
|
||||
_value_set = p_value_set
|
||||
_default = p_default_value
|
||||
_help = p_help
|
||||
|
||||
|
||||
func name() -> String:
|
||||
return _name
|
||||
|
||||
|
||||
func type() -> int:
|
||||
return _type
|
||||
|
||||
|
||||
func value() -> Variant:
|
||||
return _value
|
||||
|
||||
|
||||
func int_value() -> int:
|
||||
return _value
|
||||
|
||||
func value_as_string() -> String:
|
||||
return _value
|
||||
|
||||
|
||||
func value_set() -> PackedStringArray:
|
||||
return _value_set
|
||||
|
||||
|
||||
func is_selectable_value() -> bool:
|
||||
return not _value_set.is_empty()
|
||||
|
||||
|
||||
func set_value(p_value: Variant) -> void:
|
||||
match _type:
|
||||
TYPE_STRING:
|
||||
_value = str(p_value)
|
||||
TYPE_BOOL:
|
||||
_value = type_convert(p_value, TYPE_BOOL)
|
||||
TYPE_INT:
|
||||
_value = type_convert(p_value, TYPE_INT)
|
||||
TYPE_FLOAT:
|
||||
_value = type_convert(p_value, TYPE_FLOAT)
|
||||
TYPE_DICTIONARY:
|
||||
_value = type_convert(p_value, TYPE_DICTIONARY)
|
||||
_:
|
||||
_value = p_value
|
||||
|
||||
|
||||
func default() -> Variant:
|
||||
return _default
|
||||
|
||||
|
||||
func category() -> String:
|
||||
var elements := _name.split("/")
|
||||
if elements.size() > 3:
|
||||
return elements[2]
|
||||
return ""
|
||||
|
||||
|
||||
func help() -> String:
|
||||
return _help
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return "%-64s %-10s %-10s (%s) help:%s set:%s" % [name(), type(), value(), default(), help(), _value_set]
|
||||
0
addons/gdUnit4/src/core/GdUnitProperty.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitProperty.gd.uid
Normal file
109
addons/gdUnit4/src/core/GdUnitResult.gd
Normal file
109
addons/gdUnit4/src/core/GdUnitResult.gd
Normal file
@@ -0,0 +1,109 @@
|
||||
class_name GdUnitResult
|
||||
extends RefCounted
|
||||
|
||||
enum {
|
||||
SUCCESS,
|
||||
WARN,
|
||||
ERROR,
|
||||
EMPTY
|
||||
}
|
||||
|
||||
var _state: int
|
||||
var _warn_message := ""
|
||||
var _error_message := ""
|
||||
var _value :Variant = null
|
||||
|
||||
|
||||
static func empty() -> GdUnitResult:
|
||||
var result := GdUnitResult.new()
|
||||
result._state = EMPTY
|
||||
return result
|
||||
|
||||
|
||||
static func success(p_value: Variant = "") -> GdUnitResult:
|
||||
assert(p_value != null, "The value must not be NULL")
|
||||
var result := GdUnitResult.new()
|
||||
result._value = p_value
|
||||
result._state = SUCCESS
|
||||
return result
|
||||
|
||||
|
||||
static func warn(p_warn_message: String, p_value: Variant = null) -> GdUnitResult:
|
||||
assert(not p_warn_message.is_empty()) #,"The message must not be empty")
|
||||
var result := GdUnitResult.new()
|
||||
result._value = p_value
|
||||
result._warn_message = p_warn_message
|
||||
result._state = WARN
|
||||
return result
|
||||
|
||||
|
||||
static func error(p_error_message: String) -> GdUnitResult:
|
||||
assert(not p_error_message.is_empty(), "The message must not be empty")
|
||||
var result := GdUnitResult.new()
|
||||
result._value = null
|
||||
result._error_message = p_error_message
|
||||
result._state = ERROR
|
||||
return result
|
||||
|
||||
|
||||
func is_success() -> bool:
|
||||
return _state == SUCCESS
|
||||
|
||||
|
||||
func is_warn() -> bool:
|
||||
return _state == WARN
|
||||
|
||||
|
||||
func is_error() -> bool:
|
||||
return _state == ERROR
|
||||
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _state == EMPTY
|
||||
|
||||
|
||||
func value() -> Variant:
|
||||
return _value
|
||||
|
||||
|
||||
func value_as_string() -> String:
|
||||
return _value
|
||||
|
||||
|
||||
func or_else(p_value: Variant) -> Variant:
|
||||
if not is_success():
|
||||
return p_value
|
||||
return value()
|
||||
|
||||
|
||||
func error_message() -> String:
|
||||
return _error_message
|
||||
|
||||
|
||||
func warn_message() -> String:
|
||||
return _warn_message
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return str(GdUnitResult.serialize(self))
|
||||
|
||||
|
||||
static func serialize(result: GdUnitResult) -> Dictionary:
|
||||
if result == null:
|
||||
push_error("Can't serialize a Null object from type GdUnitResult")
|
||||
return {
|
||||
"state" : result._state,
|
||||
"value" : var_to_str(result._value),
|
||||
"warn_msg" : result._warn_message,
|
||||
"err_msg" : result._error_message
|
||||
}
|
||||
|
||||
|
||||
static func deserialize(config: Dictionary) -> GdUnitResult:
|
||||
var result := GdUnitResult.new()
|
||||
var cfg_value: String = config.get("value", "")
|
||||
result._value = str_to_var(cfg_value)
|
||||
result._warn_message = config.get("warn_msg", null)
|
||||
result._error_message = config.get("err_msg", null)
|
||||
result._state = config.get("state")
|
||||
return result
|
||||
0
addons/gdUnit4/src/core/GdUnitResult.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitResult.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitRunnerConfig.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitRunnerConfig.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSettings.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSettings.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSettings.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSettings.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSignalCollector.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSignalCollector.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSignals.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSignals.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSignals.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSignals.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSingleton.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSingleton.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitSingleton.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitSingleton.gd.uid
Normal file
97
addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd
Normal file
97
addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd
Normal file
@@ -0,0 +1,97 @@
|
||||
class_name GdUnitTestResourceLoader
|
||||
extends RefCounted
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
enum {
|
||||
GD_SUITE,
|
||||
CS_SUITE
|
||||
}
|
||||
|
||||
|
||||
static func load_test_suite(resource_path: String, script_type := GD_SUITE) -> Node:
|
||||
match script_type:
|
||||
GD_SUITE:
|
||||
return load_test_suite_gd(resource_path)
|
||||
CS_SUITE:
|
||||
return load_test_suite_cs(resource_path)
|
||||
assert("type '%s' is not implemented" % script_type)
|
||||
return null
|
||||
|
||||
|
||||
static func load_tests(resource_path: String) -> Dictionary:
|
||||
var script := load_gd_script(resource_path)
|
||||
var discovered_tests := {}
|
||||
GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void:
|
||||
discovered_tests[test.display_name] = test
|
||||
)
|
||||
|
||||
return discovered_tests
|
||||
|
||||
|
||||
static func load_test_suite_gd(resource_path: String) -> GdUnitTestSuite:
|
||||
var script := load_gd_script(resource_path)
|
||||
var discovered_tests: Array[GdUnitTestCase] = []
|
||||
GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void:
|
||||
discovered_tests.append(test)
|
||||
)
|
||||
# complete test suite wiht parsed test cases
|
||||
return GdUnitTestSuiteScanner.new().load_suite(script, discovered_tests)
|
||||
|
||||
|
||||
static func load_test_suite_cs(resource_path: String) -> Node:
|
||||
if not GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
return null
|
||||
var script :Script = ClassDB.instantiate("CSharpScript")
|
||||
script.source_code = GdUnitFileAccess.resource_as_string(resource_path)
|
||||
script.resource_path = resource_path
|
||||
script.reload()
|
||||
return null
|
||||
|
||||
|
||||
static func load_cs_script(resource_path: String, debug_write := false) -> Script:
|
||||
if not GdUnit4CSharpApiLoader.is_api_loaded():
|
||||
return null
|
||||
var script :Script = ClassDB.instantiate("CSharpScript")
|
||||
script.source_code = GdUnitFileAccess.resource_as_string(resource_path)
|
||||
var script_resource_path := resource_path.replace(resource_path.get_extension(), "cs")
|
||||
if debug_write:
|
||||
script_resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % script_resource_path.get_file()
|
||||
print_debug("save resource:", script_resource_path)
|
||||
DirAccess.remove_absolute(script_resource_path)
|
||||
var err := ResourceSaver.save(script, script_resource_path)
|
||||
if err != OK:
|
||||
print_debug("Can't save debug resource",script_resource_path, "Error:", error_string(err))
|
||||
script.take_over_path(script_resource_path)
|
||||
else:
|
||||
script.take_over_path(resource_path)
|
||||
script.reload()
|
||||
return script
|
||||
|
||||
|
||||
static func load_gd_script(resource_path: String, debug_write := false) -> GDScript:
|
||||
# grap current level
|
||||
var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access")
|
||||
# disable and load the script
|
||||
ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0)
|
||||
|
||||
var script := GDScript.new()
|
||||
script.source_code = GdUnitFileAccess.resource_as_string(resource_path)
|
||||
var script_resource_path := resource_path.replace(resource_path.get_extension(), "gd")
|
||||
if debug_write:
|
||||
script_resource_path = script_resource_path.replace("res://", GdUnitFileAccess.temp_dir() + "/")
|
||||
#print_debug("save resource: ", script_resource_path)
|
||||
DirAccess.remove_absolute(script_resource_path)
|
||||
DirAccess.make_dir_recursive_absolute(script_resource_path.get_base_dir())
|
||||
var err := ResourceSaver.save(script, script_resource_path, ResourceSaver.FLAG_REPLACE_SUBRESOURCE_PATHS)
|
||||
if err != OK:
|
||||
print_debug("Can't save debug resource", script_resource_path, "Error:", error_string(err))
|
||||
script.take_over_path(script_resource_path)
|
||||
else:
|
||||
script.take_over_path(resource_path)
|
||||
var error := script.reload()
|
||||
if error != OK:
|
||||
push_error("Errors on loading script %s. Error: %s" % [resource_path, error_string(error)])
|
||||
ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access)
|
||||
return script
|
||||
#@warning_ignore("unsafe_cast")
|
||||
0
addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd
Normal file
404
addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd
Normal file
404
addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd
Normal file
@@ -0,0 +1,404 @@
|
||||
class_name GdUnitTestSuiteScanner
|
||||
extends RefCounted
|
||||
|
||||
const TEST_FUNC_TEMPLATE ="""
|
||||
|
||||
func test_${func_name}() -> void:
|
||||
# remove this line and complete your test
|
||||
assert_not_yet_implemented()
|
||||
"""
|
||||
|
||||
|
||||
# we exclude the gdunit source directorys by default
|
||||
const exclude_scan_directories = [
|
||||
"res://addons/gdUnit4/bin",
|
||||
"res://addons/gdUnit4/src",
|
||||
"res://reports"]
|
||||
|
||||
|
||||
const ARGUMENT_TIMEOUT := "timeout"
|
||||
const ARGUMENT_SKIP := "do_skip"
|
||||
const ARGUMENT_SKIP_REASON := "skip_reason"
|
||||
const ARGUMENT_PARAMETER_SET := "test_parameters"
|
||||
|
||||
|
||||
var _script_parser := GdScriptParser.new()
|
||||
var _included_resources: PackedStringArray = []
|
||||
var _excluded_resources: PackedStringArray = []
|
||||
var _expression_runner := GdUnitExpressionRunner.new()
|
||||
var _regex_extends_clazz_name := RegEx.create_from_string("extends[\\s]+([\\S]+)")
|
||||
|
||||
|
||||
func prescan_testsuite_classes() -> void:
|
||||
# scan and cache extends GdUnitTestSuite by class name an resource paths
|
||||
var script_classes: Array[Dictionary] = ProjectSettings.get_global_class_list()
|
||||
for script_meta in script_classes:
|
||||
var base_class: String = script_meta["base"]
|
||||
var resource_path: String = script_meta["path"]
|
||||
if base_class == "GdUnitTestSuite":
|
||||
@warning_ignore("return_value_discarded")
|
||||
_included_resources.append(resource_path)
|
||||
elif ClassDB.class_exists(base_class):
|
||||
@warning_ignore("return_value_discarded")
|
||||
_excluded_resources.append(resource_path)
|
||||
|
||||
|
||||
func scan(resource_path: String) -> Array[Script]:
|
||||
prescan_testsuite_classes()
|
||||
# if single testsuite requested
|
||||
if FileAccess.file_exists(resource_path):
|
||||
var test_suite := _load_is_test_suite(resource_path)
|
||||
if test_suite != null:
|
||||
return [test_suite]
|
||||
return []
|
||||
return scan_directory(resource_path)
|
||||
|
||||
|
||||
func scan_directory(resource_path: String) -> Array[Script]:
|
||||
prescan_testsuite_classes()
|
||||
# We use the global cache to fast scan for test suites.
|
||||
if _excluded_resources.has(resource_path):
|
||||
return []
|
||||
|
||||
var base_dir := DirAccess.open(resource_path)
|
||||
if base_dir == null:
|
||||
prints("Given directory or file does not exists:", resource_path)
|
||||
return []
|
||||
|
||||
prints("Scanning for test suites in:", resource_path)
|
||||
return _scan_test_suites_scripts(base_dir, [])
|
||||
|
||||
|
||||
func _scan_test_suites_scripts(dir: DirAccess, collected_suites: Array[Script]) -> Array[Script]:
|
||||
if exclude_scan_directories.has(dir.get_current_dir()):
|
||||
return collected_suites
|
||||
var err := dir.list_dir_begin()
|
||||
if err != OK:
|
||||
push_error("Error on scanning directory %s" % dir.get_current_dir(), error_string(err))
|
||||
return collected_suites
|
||||
var file_name := dir.get_next()
|
||||
while file_name != "":
|
||||
var resource_path := GdUnitTestSuiteScanner._file(dir, file_name)
|
||||
if dir.current_is_dir():
|
||||
var sub_dir := DirAccess.open(resource_path)
|
||||
if sub_dir != null:
|
||||
@warning_ignore("return_value_discarded")
|
||||
_scan_test_suites_scripts(sub_dir, collected_suites)
|
||||
else:
|
||||
var time := LocalTime.now()
|
||||
var test_suite := _load_is_test_suite(resource_path)
|
||||
if test_suite:
|
||||
collected_suites.append(test_suite)
|
||||
if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300:
|
||||
push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since())
|
||||
file_name = dir.get_next()
|
||||
return collected_suites
|
||||
|
||||
|
||||
static func _file(dir: DirAccess, file_name: String) -> String:
|
||||
var current_dir := dir.get_current_dir()
|
||||
if current_dir.ends_with("/"):
|
||||
return current_dir + file_name
|
||||
return current_dir + "/" + file_name
|
||||
|
||||
|
||||
func _load_is_test_suite(resource_path: String) -> Script:
|
||||
if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path):
|
||||
return null
|
||||
|
||||
# We use the global cache to fast scan for test suites.
|
||||
if _excluded_resources.has(resource_path):
|
||||
return null
|
||||
# Check in the global class cache whether the GdUnitTestSuite class has been extended.
|
||||
if _included_resources.has(resource_path):
|
||||
return GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path)
|
||||
|
||||
# Otherwise we need to scan manual, we need to exclude classes where direct extends form Godot classes
|
||||
# the resource loader can fail to load e.g. plugin classes with do preload other scripts
|
||||
#var extends_from := get_extends_classname(resource_path)
|
||||
# If not extends is defined or extends from a Godot class
|
||||
#if extends_from.is_empty() or ClassDB.class_exists(extends_from):
|
||||
# return null
|
||||
# Finally, we need to load the class to determine it is a test suite
|
||||
var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path)
|
||||
if not is_test_suite(script):
|
||||
return null
|
||||
return script
|
||||
|
||||
|
||||
func load_suite(script: GDScript, tests: Array[GdUnitTestCase]) -> GdUnitTestSuite:
|
||||
var test_suite: GdUnitTestSuite = script.new()
|
||||
var first_test: GdUnitTestCase = tests.front()
|
||||
test_suite.set_name(first_test.suite_name)
|
||||
|
||||
# We need to group first all parameterized tests together to load the parameter set once
|
||||
var grouped_by_test := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String:
|
||||
return test.test_name
|
||||
)
|
||||
# Extract function descriptors
|
||||
var test_names: PackedStringArray = grouped_by_test.keys()
|
||||
test_names.append("before")
|
||||
var function_descriptors := _script_parser.get_function_descriptors(script, test_names)
|
||||
|
||||
# Convert to test
|
||||
for fd in function_descriptors:
|
||||
if fd.name() == "before":
|
||||
_handle_test_suite_arguments(test_suite, script, fd)
|
||||
continue
|
||||
|
||||
# Build test attributes from test method
|
||||
var test_attribute := _build_test_attribute(script, fd)
|
||||
# Create test from descriptor and given attributes
|
||||
var test_group: Array = grouped_by_test[fd.name()]
|
||||
for test: GdUnitTestCase in test_group:
|
||||
# We need a copy, because of mutable state
|
||||
var attribute: TestCaseAttribute = test_attribute.clone()
|
||||
test_suite.add_child(_TestCase.new(test, attribute, fd))
|
||||
return test_suite
|
||||
|
||||
|
||||
func _build_test_attribute(script: GDScript, fd: GdFunctionDescriptor) -> TestCaseAttribute:
|
||||
var collected_unknown_aruments := PackedStringArray()
|
||||
var attribute := TestCaseAttribute.new()
|
||||
|
||||
# Collect test attributes
|
||||
for arg: GdFunctionArgument in fd.args():
|
||||
if arg.type() == GdObjects.TYPE_FUZZER:
|
||||
attribute.fuzzers.append(arg)
|
||||
else:
|
||||
# We allow underscore as prefix to prevent unused argument warnings
|
||||
match arg.name().trim_prefix("_"):
|
||||
ARGUMENT_TIMEOUT:
|
||||
attribute.timeout = type_convert(arg.default(), TYPE_INT)
|
||||
ARGUMENT_SKIP:
|
||||
var result: Variant = _expression_runner.execute(script, arg.plain_value())
|
||||
if result is bool:
|
||||
attribute.is_skipped = result
|
||||
else:
|
||||
push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value())
|
||||
ARGUMENT_SKIP_REASON:
|
||||
attribute.skip_reason = arg.plain_value()
|
||||
Fuzzer.ARGUMENT_ITERATIONS:
|
||||
attribute.fuzzer_iterations = type_convert(arg.default(), TYPE_INT)
|
||||
Fuzzer.ARGUMENT_SEED:
|
||||
attribute.test_seed = type_convert(arg.default(), TYPE_INT)
|
||||
ARGUMENT_PARAMETER_SET:
|
||||
collected_unknown_aruments.clear()
|
||||
pass
|
||||
_:
|
||||
collected_unknown_aruments.append(arg.name())
|
||||
|
||||
# Verify for unknown arguments
|
||||
if not collected_unknown_aruments.is_empty():
|
||||
attribute.is_skipped = true
|
||||
attribute.skip_reason = "Unknown test case argument's %s found." % collected_unknown_aruments
|
||||
|
||||
return attribute
|
||||
|
||||
|
||||
# We load the test suites with disabled unsafe_method_access to avoid spamming loading errors
|
||||
# `unsafe_method_access` will happen when using `assert_that`
|
||||
static func load_with_disabled_warnings(resource_path: String) -> Script:
|
||||
# grap current level
|
||||
var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access")
|
||||
|
||||
# disable and load the script
|
||||
ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0)
|
||||
|
||||
var script: Script = (
|
||||
GdUnitTestResourceLoader.load_gd_script(resource_path) if resource_path.ends_with("resource")
|
||||
else ResourceLoader.load(resource_path))
|
||||
|
||||
# restore
|
||||
ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access)
|
||||
return script
|
||||
|
||||
|
||||
static func is_test_suite(script: Script) -> bool:
|
||||
if script is GDScript:
|
||||
var stack := [script]
|
||||
while not stack.is_empty():
|
||||
var current: Script = stack.pop_front()
|
||||
var base: Script = current.get_base_script()
|
||||
if base != null:
|
||||
if base.resource_path.find("GdUnitTestSuite") != -1:
|
||||
return true
|
||||
stack.push_back(base)
|
||||
elif script != null and script.get_class() == "CSharpScript":
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _is_script_format_supported(resource_path: String) -> bool:
|
||||
var ext := resource_path.get_extension()
|
||||
return ext == "gd" or ext == "cs"
|
||||
|
||||
|
||||
static func parse_test_suite_name(script: Script) -> String:
|
||||
return script.resource_path.get_file().replace(".gd", "")
|
||||
|
||||
|
||||
func _handle_test_suite_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void:
|
||||
for arg in fd.args():
|
||||
# We allow underscore as prefix to prevent unused argument warnings
|
||||
match arg.name().trim_prefix("_"):
|
||||
ARGUMENT_SKIP:
|
||||
var result: Variant = _expression_runner.execute(script, arg.plain_value())
|
||||
if result is bool:
|
||||
test_suite.__is_skipped = result
|
||||
else:
|
||||
push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value())
|
||||
ARGUMENT_SKIP_REASON:
|
||||
test_suite.__skip_reason = arg.plain_value()
|
||||
_:
|
||||
push_error("Unsuported argument `%s` found on before() at '%s'!" % [arg.name(), script.resource_path])
|
||||
|
||||
|
||||
# converts given file name by configured naming convention
|
||||
static func _to_naming_convention(file_name: String) -> String:
|
||||
var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, 0)
|
||||
match nc:
|
||||
GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT:
|
||||
if GdObjects.is_snake_case(file_name):
|
||||
return GdObjects.to_snake_case(file_name + "Test")
|
||||
return GdObjects.to_pascal_case(file_name + "Test")
|
||||
GdUnitSettings.NAMING_CONVENTIONS.SNAKE_CASE:
|
||||
return GdObjects.to_snake_case(file_name + "Test")
|
||||
GdUnitSettings.NAMING_CONVENTIONS.PASCAL_CASE:
|
||||
return GdObjects.to_pascal_case(file_name + "Test")
|
||||
push_error("Unexpected case")
|
||||
return "-<Unexpected>-"
|
||||
|
||||
|
||||
static func resolve_test_suite_path(source_script_path: String, test_root_folder: String = "test") -> String:
|
||||
var file_name := source_script_path.get_basename().get_file()
|
||||
var suite_name := _to_naming_convention(file_name)
|
||||
if test_root_folder.is_empty() or test_root_folder == "/":
|
||||
return source_script_path.replace(file_name, suite_name)
|
||||
|
||||
# is user tmp
|
||||
if source_script_path.begins_with("user://tmp"):
|
||||
return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name)
|
||||
|
||||
# at first look up is the script under a "src" folder located
|
||||
var test_suite_path: String
|
||||
var src_folder := source_script_path.find("/src/")
|
||||
if src_folder != -1:
|
||||
test_suite_path = source_script_path.replace("/src/", "/"+test_root_folder+"/")
|
||||
else:
|
||||
var paths := source_script_path.split("/", false)
|
||||
# is a plugin script?
|
||||
if paths[1] == "addons":
|
||||
test_suite_path = "%s//addons/%s/%s" % [paths[0], paths[2], test_root_folder]
|
||||
# rebuild plugin path
|
||||
for index in range(3, paths.size()):
|
||||
test_suite_path += "/" + paths[index]
|
||||
else:
|
||||
test_suite_path = paths[0] + "//" + test_root_folder
|
||||
for index in range(1, paths.size()):
|
||||
test_suite_path += "/" + paths[index]
|
||||
return normalize_path(test_suite_path).replace(file_name, suite_name)
|
||||
|
||||
|
||||
static func normalize_path(path: String) -> String:
|
||||
return path.replace("///", "/")
|
||||
|
||||
|
||||
static func create_test_suite(test_suite_path: String, source_path: String) -> GdUnitResult:
|
||||
# create directory if not exists
|
||||
if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()):
|
||||
var error_ := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir())
|
||||
if error_ != OK:
|
||||
return GdUnitResult.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error_])
|
||||
var script := GDScript.new()
|
||||
script.source_code = GdUnitTestSuiteTemplate.build_template(source_path)
|
||||
var error := ResourceSaver.save(script, test_suite_path)
|
||||
if error != OK:
|
||||
return GdUnitResult.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error])
|
||||
return GdUnitResult.success(test_suite_path)
|
||||
|
||||
|
||||
static func get_test_case_line_number(resource_path: String, func_name: String) -> int:
|
||||
var file := FileAccess.open(resource_path, FileAccess.READ)
|
||||
if file != null:
|
||||
var line_number := 0
|
||||
while not file.eof_reached():
|
||||
var row := file.get_line()
|
||||
line_number += 1
|
||||
# ignore comments and empty lines and not test functions
|
||||
if row.begins_with("#") || row.length() == 0 || row.find("func test_") == -1:
|
||||
continue
|
||||
# abort if test case name found
|
||||
if row.find("func") != -1 and row.find("test_" + func_name) != -1:
|
||||
return line_number
|
||||
return -1
|
||||
|
||||
|
||||
func get_extends_classname(resource_path: String) -> String:
|
||||
var file := FileAccess.open(resource_path, FileAccess.READ)
|
||||
if file != null:
|
||||
while not file.eof_reached():
|
||||
var row := file.get_line()
|
||||
# skip comments and empty lines
|
||||
if row.begins_with("#") || row.length() == 0:
|
||||
continue
|
||||
# Stop at first function
|
||||
if row.contains("func"):
|
||||
return ""
|
||||
var result := _regex_extends_clazz_name.search(row)
|
||||
if result != null:
|
||||
return result.get_string(1)
|
||||
return ""
|
||||
|
||||
|
||||
static func add_test_case(resource_path: String, func_name: String) -> GdUnitResult:
|
||||
var script := load_with_disabled_warnings(resource_path)
|
||||
# count all exiting lines and add two as space to add new test case
|
||||
var line_number := count_lines(script) + 2
|
||||
var func_body := TEST_FUNC_TEMPLATE.replace("${func_name}", func_name)
|
||||
if Engine.is_editor_hint():
|
||||
# NOTE: Avoid using EditorInterface and EditorSettings directly,
|
||||
# as it causes compilation errors in exported projects.
|
||||
@warning_ignore_start("unsafe_method_access")
|
||||
var editor_interface: Object = Engine.get_singleton("EditorInterface")
|
||||
var settings: Object = editor_interface.get_editor_settings()
|
||||
var ident_type: int = settings.get_setting("text_editor/behavior/indent/type")
|
||||
var ident_size: int = settings.get_setting("text_editor/behavior/indent/size")
|
||||
@warning_ignore_restore("unsafe_method_access")
|
||||
if ident_type == 1:
|
||||
func_body = func_body.replace(" ", "".lpad(ident_size, " "))
|
||||
script.source_code += func_body
|
||||
var error := ResourceSaver.save(script, resource_path)
|
||||
if error != OK:
|
||||
return GdUnitResult.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error])
|
||||
return GdUnitResult.success({ "path" : resource_path, "line" : line_number})
|
||||
|
||||
|
||||
static func count_lines(script: Script) -> int:
|
||||
return script.source_code.split("\n").size()
|
||||
|
||||
|
||||
static func test_suite_exists(test_suite_path: String) -> bool:
|
||||
return FileAccess.file_exists(test_suite_path)
|
||||
|
||||
|
||||
static func test_case_exists(test_suite_path :String, func_name :String) -> bool:
|
||||
if not test_suite_exists(test_suite_path):
|
||||
return false
|
||||
var script := load_with_disabled_warnings(test_suite_path)
|
||||
for f in script.get_script_method_list():
|
||||
if f["name"] == "test_" + func_name:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func create_test_case(test_suite_path: String, func_name: String, source_script_path: String) -> GdUnitResult:
|
||||
if test_case_exists(test_suite_path, func_name):
|
||||
var line_number := get_test_case_line_number(test_suite_path, func_name)
|
||||
return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number})
|
||||
|
||||
if not test_suite_exists(test_suite_path):
|
||||
var result := create_test_suite(test_suite_path, source_script_path)
|
||||
if result.is_error():
|
||||
return result
|
||||
return add_test_case(test_suite_path, func_name)
|
||||
0
addons/gdUnit4/src/core/GdUnitTools.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitTools.gd
Normal file
0
addons/gdUnit4/src/core/GdUnitTools.gd.uid
Normal file
0
addons/gdUnit4/src/core/GdUnitTools.gd.uid
Normal file
0
addons/gdUnit4/src/core/GodotVersionFixures.gd
Normal file
0
addons/gdUnit4/src/core/GodotVersionFixures.gd
Normal file
0
addons/gdUnit4/src/core/GodotVersionFixures.gd.uid
Normal file
0
addons/gdUnit4/src/core/GodotVersionFixures.gd.uid
Normal file
0
addons/gdUnit4/src/core/LocalTime.gd
Normal file
0
addons/gdUnit4/src/core/LocalTime.gd
Normal file
0
addons/gdUnit4/src/core/LocalTime.gd.uid
Normal file
0
addons/gdUnit4/src/core/LocalTime.gd.uid
Normal file
0
addons/gdUnit4/src/core/_TestCase.gd
Normal file
0
addons/gdUnit4/src/core/_TestCase.gd
Normal file
0
addons/gdUnit4/src/core/_TestCase.gd.uid
Normal file
0
addons/gdUnit4/src/core/_TestCase.gd.uid
Normal file
0
addons/gdUnit4/src/core/assets/touch-button.png
Normal file
0
addons/gdUnit4/src/core/assets/touch-button.png
Normal file
0
addons/gdUnit4/src/core/command/GdUnitCommand.gd
Normal file
0
addons/gdUnit4/src/core/command/GdUnitCommand.gd
Normal file
0
addons/gdUnit4/src/core/command/GdUnitShortcut.gd
Normal file
0
addons/gdUnit4/src/core/command/GdUnitShortcut.gd
Normal file
0
addons/gdUnit4/src/core/discovery/GdUnitGUID.gd
Normal file
0
addons/gdUnit4/src/core/discovery/GdUnitGUID.gd
Normal file
0
addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid
Normal file
0
addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid
Normal file
125
addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd
Normal file
125
addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd
Normal file
@@ -0,0 +1,125 @@
|
||||
## GdUnitTestCase
|
||||
## A class representing a single test case in GdUnit4.
|
||||
## This class is used as a data container to hold all relevant information about a test case,
|
||||
## including its location, dependencies, and metadata for test discovery and execution.
|
||||
|
||||
class_name GdUnitTestCase
|
||||
extends RefCounted
|
||||
|
||||
## A unique identifier for the test case. Used to track and reference specific test instances.
|
||||
var guid := GdUnitGUID.new()
|
||||
|
||||
## The resource path to the test suite
|
||||
var suite_resource_path: String
|
||||
|
||||
## The name of the test method/function. Should start with "test_" prefix.
|
||||
var test_name: String
|
||||
|
||||
## The class name of the test suite containing this test case.
|
||||
var suite_name: String
|
||||
|
||||
## The fully qualified name of the test case following C# namespace pattern:
|
||||
## Constructed from the folder path (where folders are dot-separated), the test suite name, and the test case name.
|
||||
## All parts are joined by dots: {folder1.folder2.folder3}.{suite_name}.{test_name}
|
||||
var fully_qualified_name: String
|
||||
|
||||
var display_name: String
|
||||
|
||||
## Index tracking test attributes for ordered execution. Default is 0.
|
||||
## Higher values indicate later execution in the test sequence.
|
||||
var attribute_index: int
|
||||
|
||||
## Flag indicating if this test requires the Godot runtime environment.
|
||||
## Tests requiring runtime cannot be executed in isolation.
|
||||
var require_godot_runtime: bool = true
|
||||
|
||||
## The path to the source file containing this test case.
|
||||
## Used for test discovery and execution.
|
||||
var source_file: String
|
||||
|
||||
## Optional holds the assembly location for C# tests
|
||||
var assembly_location: String = ""
|
||||
|
||||
## The line number where the test case is defined in the source file.
|
||||
## Used for navigation and error reporting.
|
||||
var line_number: int = -1
|
||||
|
||||
## Additional metadata about the test case, such as:
|
||||
## - tags: Array[String] - Test categories/tags for filtering
|
||||
## - timeout: int - Maximum execution time in milliseconds
|
||||
## - skip: bool - Whether the test should be skipped
|
||||
## - dependencies: Array[String] - Required test dependencies
|
||||
var metadata: Dictionary = {}
|
||||
|
||||
|
||||
static func from_dict(dict: Dictionary) -> GdUnitTestCase:
|
||||
var test := GdUnitTestCase.new()
|
||||
test.guid = GdUnitGUID.new(str(dict["guid"]))
|
||||
test.suite_resource_path = dict["suite_resource_path"] if dict.has("suite_resource_path") else dict["source_file"]
|
||||
test.suite_name = dict["managed_type"]
|
||||
test.test_name = dict["test_name"]
|
||||
test.display_name = dict["simple_name"]
|
||||
test.fully_qualified_name = dict["fully_qualified_name"]
|
||||
test.attribute_index = dict["attribute_index"]
|
||||
test.source_file = dict["source_file"]
|
||||
test.line_number = dict["line_number"]
|
||||
test.require_godot_runtime = dict["require_godot_runtime"]
|
||||
test.assembly_location = dict["assembly_location"]
|
||||
return test
|
||||
|
||||
|
||||
static func to_dict(test: GdUnitTestCase) -> Dictionary:
|
||||
return {
|
||||
"guid": test.guid._guid,
|
||||
"suite_resource_path": test.suite_resource_path,
|
||||
"managed_type": test.suite_name,
|
||||
"test_name" : test.test_name,
|
||||
"simple_name" : test.display_name,
|
||||
"fully_qualified_name" : test.fully_qualified_name,
|
||||
"attribute_index" : test.attribute_index,
|
||||
"source_file" : test.source_file,
|
||||
"line_number" : test.line_number,
|
||||
"require_godot_runtime" : test.require_godot_runtime,
|
||||
"assembly_location" : test.assembly_location
|
||||
}
|
||||
|
||||
|
||||
static func from(_suite_resource_path: String, _source_file: String, _line_number: int, _test_name: String, _attribute_index := -1, _test_parameters := "") -> GdUnitTestCase:
|
||||
if(_source_file == null or _source_file.is_empty()):
|
||||
prints(_test_name)
|
||||
|
||||
assert(_test_name != null and not _test_name.is_empty(), "Precondition: The parameter 'test_name' is not set")
|
||||
assert(_source_file != null and not _source_file.is_empty(), "Precondition: The parameter 'source_file' is not set")
|
||||
|
||||
var test := GdUnitTestCase.new()
|
||||
test.suite_resource_path = _suite_resource_path
|
||||
test.test_name = _test_name
|
||||
test.source_file = _source_file
|
||||
test.line_number = _line_number
|
||||
test.attribute_index = _attribute_index
|
||||
test._build_suite_name()
|
||||
test._build_display_name(_test_parameters)
|
||||
test._build_fully_qualified_name(_suite_resource_path)
|
||||
return test
|
||||
|
||||
|
||||
func _build_suite_name() -> void:
|
||||
suite_name = source_file.get_file().get_basename()
|
||||
assert(suite_name != null and not suite_name.is_empty(), "Precondition: The parameter 'suite_name' can't be resolved")
|
||||
|
||||
|
||||
func _build_display_name(_test_parameters: String) -> void:
|
||||
if attribute_index == -1:
|
||||
display_name = test_name
|
||||
else:
|
||||
display_name = "%s:%d (%s)" % [test_name, attribute_index, _test_parameters.trim_prefix("[").trim_suffix("]").replace('"', "'")]
|
||||
|
||||
|
||||
func _build_fully_qualified_name(_resource_path: String) -> void:
|
||||
var name_space := _resource_path.trim_prefix("res://").trim_suffix(".gd").trim_suffix(".cs").replace("/", ".")
|
||||
|
||||
if attribute_index == -1:
|
||||
fully_qualified_name = "%s.%s" % [name_space, test_name]
|
||||
else:
|
||||
fully_qualified_name = "%s.%s.%s" % [name_space, test_name, display_name]
|
||||
assert(fully_qualified_name != null and not fully_qualified_name.is_empty(), "Precondition: The parameter 'fully_qualified_name' can't be resolved")
|
||||
323
addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd
Normal file
323
addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd
Normal file
@@ -0,0 +1,323 @@
|
||||
## Guards and tracks test case changes during test discovery and file modifications.[br]
|
||||
## [br]
|
||||
## This guard maintains a cache of discovered tests to track changes between test runs and during[br]
|
||||
## file modifications. It is optimized for performance using simple but effective test identity checks.[br]
|
||||
## [br]
|
||||
## Test Change Detection:[br]
|
||||
## - Moved tests: The test implementation remains at a different line number[br]
|
||||
## - Renamed tests: The test line position remains but the test name changed[br]
|
||||
## - Deleted tests: A previously discovered test was removed[br]
|
||||
## - Added tests: A new test was discovered[br]
|
||||
## [br]
|
||||
## Cache Management:[br]
|
||||
## - Maintains test identity through unique GdUnitTestCase GUIDs[br]
|
||||
## - Maps source files to their discovered test cases[br]
|
||||
## - Tracks only essential metadata (line numbers, names) to minimize memory use[br]
|
||||
## [br]
|
||||
## Change Detection Strategy:[br]
|
||||
## The guard uses a lightweight approach by comparing only line numbers and test names.[br]
|
||||
## This avoids expensive operations like test content parsing or similarity checks.[br]
|
||||
## [br]
|
||||
## Event Handling:[br]
|
||||
## - Emits events on test changes through GdUnitSignals[br]
|
||||
## - Synchronizes cache with test discovery events[br]
|
||||
## - Notifies UI about test changes[br]
|
||||
## [br]
|
||||
## Example usage:[br]
|
||||
## [codeblock]
|
||||
## # Create guard for tracking test changes
|
||||
## var guard := GdUnitTestDiscoverGuard.new()
|
||||
##
|
||||
## # Connect to test discovery events
|
||||
## GdUnitSignals.instance().gdunit_test_discovered.connect(guard.sync_test_added)
|
||||
##
|
||||
## # Discover tests and track changes
|
||||
## await guard.discover(test_script)
|
||||
## [/codeblock]
|
||||
class_name GdUnitTestDiscoverGuard
|
||||
extends Object
|
||||
|
||||
|
||||
|
||||
static func instance() -> GdUnitTestDiscoverGuard:
|
||||
return GdUnitSingleton.instance("GdUnitTestDiscoverGuard", func() -> GdUnitTestDiscoverGuard:
|
||||
return GdUnitTestDiscoverGuard.new()
|
||||
)
|
||||
|
||||
|
||||
## Maps source files to their discovered test cases.[br]
|
||||
## [br]
|
||||
## Key: Test suite source file path[br]
|
||||
## Value: Array of [class GdUnitTestCase] instances
|
||||
var _discover_cache := {}
|
||||
|
||||
|
||||
## Tracks discovered test changes for debug purposes.[br]
|
||||
## [br]
|
||||
## Available in debug mode only. Contains dictionaries:[br]
|
||||
## - changed_tests: Tests that were moved or renamed[br]
|
||||
## - deleted_tests: Tests that were removed[br]
|
||||
## - added_tests: New tests that were discovered
|
||||
var _discovered_changes := {}
|
||||
|
||||
|
||||
## Controls test change debug tracking.[br]
|
||||
## [br]
|
||||
## When true, maintains _discovered_changes for debugging.[br]
|
||||
## Used primarily in tests to verify change detection.
|
||||
var _is_debug := false
|
||||
|
||||
|
||||
## Creates a new guard instance.[br]
|
||||
## [br]
|
||||
## [param is_debug] When true, enables change tracking for debugging.
|
||||
func _init(is_debug := false) -> void:
|
||||
_is_debug = is_debug
|
||||
# Register for discovery events to sync the cache
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitSignals.instance().gdunit_test_discover_added.connect(sync_test_added)
|
||||
GdUnitSignals.instance().gdunit_test_discover_deleted.connect(sync_test_deleted)
|
||||
GdUnitSignals.instance().gdunit_test_discover_modified.connect(sync_test_modified)
|
||||
GdUnitSignals.instance().gdunit_event.connect(handle_discover_events)
|
||||
|
||||
|
||||
## Adds a discovered test to the cache.[br]
|
||||
## [br]
|
||||
## [param test_case] The test case to add to the cache.
|
||||
func sync_test_added(test_case: GdUnitTestCase) -> void:
|
||||
var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(test_case.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase))
|
||||
test_cases.append(test_case)
|
||||
|
||||
|
||||
## Removes a test from the cache.[br]
|
||||
## [br]
|
||||
## [param test_case] The test case to remove from the cache.
|
||||
func sync_test_deleted(test_case: GdUnitTestCase) -> void:
|
||||
var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(test_case.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase))
|
||||
test_cases.erase(test_case)
|
||||
|
||||
|
||||
## Updates a test from the cache.[br]
|
||||
## [br]
|
||||
## [param test_case] The test case to update from the cache.
|
||||
func sync_test_modified(changed_test: GdUnitTestCase) -> void:
|
||||
var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(changed_test.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase))
|
||||
for test in test_cases:
|
||||
if test.guid == changed_test.guid:
|
||||
test.test_name = changed_test.test_name
|
||||
test.display_name = changed_test.display_name
|
||||
test.line_number = changed_test.line_number
|
||||
break
|
||||
|
||||
|
||||
## Handles test discovery events.[br]
|
||||
## [br]
|
||||
## Resets the cache when a new discovery starts.[br]
|
||||
## [param event] The discovery event to handle.
|
||||
func handle_discover_events(event: GdUnitEvent) -> void:
|
||||
# reset the cache on fresh discovery
|
||||
if event.type() == GdUnitEvent.DISCOVER_START:
|
||||
_discover_cache = {}
|
||||
|
||||
|
||||
## Registers a callback for discovered tests.[br]
|
||||
## [br]
|
||||
## Default sink writes to [class GdUnitTestDiscoverSink].
|
||||
static func default_discover_sink(test_case: GdUnitTestCase) -> void:
|
||||
GdUnitTestDiscoverSink.discover(test_case)
|
||||
|
||||
|
||||
## Finds a test case by its unique identifier.[br]
|
||||
## [br]
|
||||
## Searches through all cached test cases across all test suites[br]
|
||||
## to find a test with the matching GUID.[br]
|
||||
## [br]
|
||||
## [param id] The GUID of the test to find[br]
|
||||
## Returns the matching test case or null if not found.
|
||||
func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase:
|
||||
for test_sets: Array[GdUnitTestCase] in _discover_cache.values():
|
||||
for test in test_sets:
|
||||
if test.guid.equals(id):
|
||||
return test
|
||||
|
||||
return null
|
||||
|
||||
|
||||
func get_discovered_tests() -> Array[GdUnitTestCase]:
|
||||
var discovered_tests: Array[GdUnitTestCase] = []
|
||||
for test_sets: Array[GdUnitTestCase] in _discover_cache.values():
|
||||
discovered_tests.append_array(test_sets)
|
||||
return discovered_tests
|
||||
|
||||
|
||||
## Discovers tests in a script and tracks changes.[br]
|
||||
## [br]
|
||||
## Handles both GDScript and C# test suites.[br]
|
||||
## The guard maintains test identity through changes.[br]
|
||||
## [br]
|
||||
## [param script] The test script to analyze[br]
|
||||
## [param discover_sink] Optional callback for test discovery events
|
||||
func discover(script: Script, discover_sink: Callable = default_discover_sink) -> void:
|
||||
# Verify the script has no errors before run test discovery
|
||||
var result := script.reload(true)
|
||||
if result != OK:
|
||||
return
|
||||
|
||||
if _is_debug:
|
||||
_discovered_changes["changed_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
_discovered_changes["deleted_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
_discovered_changes["added_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
|
||||
if GdUnitTestSuiteScanner.is_test_suite(script):
|
||||
# for cs scripts we need to recomplie before discover new tests
|
||||
if script.get_class() == "CSharpScript":
|
||||
await rebuild_project(script)
|
||||
|
||||
# rediscover all tests
|
||||
var source_file := script.resource_path
|
||||
var discovered_tests: Array[GdUnitTestCase] = []
|
||||
|
||||
GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void:
|
||||
discovered_tests.append(test_case)
|
||||
)
|
||||
|
||||
# The suite is never discovered, we add all discovered tests
|
||||
if not _discover_cache.has(source_file):
|
||||
for test_case in discovered_tests:
|
||||
discover_sink.call(test_case)
|
||||
return
|
||||
|
||||
sync_moved_tests(source_file, discovered_tests)
|
||||
sync_renamed_tests(source_file, discovered_tests)
|
||||
sync_deleted_tests(source_file, discovered_tests)
|
||||
sync_added_tests(source_file, discovered_tests, discover_sink)
|
||||
|
||||
|
||||
## Synchronizes moved tests between discover cycles.[br]
|
||||
## [br]
|
||||
## A test is considered moved when:[br]
|
||||
## - It has the same name[br]
|
||||
## - But a different line number[br]
|
||||
## [br]
|
||||
## [param source_file] suite source path[br]
|
||||
## [param discovered_tests] Newly discovered tests
|
||||
func sync_moved_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate()
|
||||
for discovered_test in discovered_tests:
|
||||
# lookup in cache
|
||||
var original_tests: Array[GdUnitTestCase] = cache.filter(is_test_moved.bind(discovered_test))
|
||||
for test in original_tests:
|
||||
# update the line_number
|
||||
var line_number_before := test.line_number
|
||||
test.line_number = discovered_test.line_number
|
||||
GdUnitSignals.instance().gdunit_test_discover_modified.emit(test)
|
||||
if _is_debug:
|
||||
prints("-> moved test id:%s %s: line:(%d -> %d)" % [test.guid, test.display_name, line_number_before, test.line_number])
|
||||
@warning_ignore("unsafe_method_access")
|
||||
_discovered_changes.get_or_add("changed_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test)
|
||||
|
||||
|
||||
## Synchronizes renamed tests between discover cycles.[br]
|
||||
## [br]
|
||||
## A test is considered renamed when:[br]
|
||||
## - It has the same line number[br]
|
||||
## - But a different name[br]
|
||||
## [br]
|
||||
## [param source_file] suite source path[br]
|
||||
## [param discovered_tests] Newly discovered tests
|
||||
func sync_renamed_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate()
|
||||
for discovered_test in discovered_tests:
|
||||
# lookup in cache
|
||||
var original_tests: Array[GdUnitTestCase] = cache.filter(is_test_renamed.bind(discovered_test))
|
||||
for test in original_tests:
|
||||
# update the renaming names
|
||||
var original_display_name := test.display_name
|
||||
test.test_name = discovered_test.test_name
|
||||
test.display_name = discovered_test.display_name
|
||||
GdUnitSignals.instance().gdunit_test_discover_modified.emit(test)
|
||||
if _is_debug:
|
||||
prints("-> renamed test id:%s %s -> %s" % [test.guid, original_display_name, test.display_name])
|
||||
@warning_ignore("unsafe_method_access")
|
||||
_discovered_changes.get_or_add("changed_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test)
|
||||
|
||||
|
||||
## Synchronizes deleted tests between discover cycles.[br]
|
||||
## [br]
|
||||
## A test is considered deleted when:[br]
|
||||
## - It exists in the cache[br]
|
||||
## - But is not found in the newly discovered tests[br]
|
||||
## [br]
|
||||
## [param source_file] suite source path[br]
|
||||
## [param discovered_tests] Newly discovered tests
|
||||
func sync_deleted_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate()
|
||||
# lookup in cache
|
||||
for test in cache:
|
||||
if not discovered_tests.any(test_equals.bind(test)):
|
||||
GdUnitSignals.instance().gdunit_test_discover_deleted.emit(test)
|
||||
if _is_debug:
|
||||
prints("-> deleted test id:%s %s:%d" % [test.guid, test.display_name, test.line_number])
|
||||
@warning_ignore("unsafe_method_access")
|
||||
_discovered_changes.get_or_add("deleted_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test)
|
||||
|
||||
|
||||
## Synchronizes newly added tests between discover cycles.[br]
|
||||
## [br]
|
||||
## A test is considered added when:[br]
|
||||
## - It exists in the newly discovered tests[br]
|
||||
## - But is not found in the cache[br]
|
||||
## [br]
|
||||
## [param source_file] suite source path[br]
|
||||
## [param discovered_tests] Newly discovered tests[br]
|
||||
## [param discover_sink] Callback to handle newly discovered tests
|
||||
func sync_added_tests(source_file: String, discovered_tests: Array[GdUnitTestCase], discover_sink: Callable) -> void:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate()
|
||||
# lookup in cache
|
||||
for test in discovered_tests:
|
||||
if not cache.any(test_equals.bind(test)):
|
||||
discover_sink.call(test)
|
||||
if _is_debug:
|
||||
prints("-> added test id:%s %s:%d" % [test.guid, test.display_name, test.line_number])
|
||||
@warning_ignore("unsafe_method_access")
|
||||
_discovered_changes.get_or_add("added_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test)
|
||||
|
||||
|
||||
func is_test_renamed(left: GdUnitTestCase, right: GdUnitTestCase) -> bool:
|
||||
return left.line_number == right.line_number and left.test_name != right.test_name
|
||||
|
||||
|
||||
func is_test_moved(left: GdUnitTestCase, right: GdUnitTestCase) -> bool:
|
||||
return left.line_number != right.line_number and left.test_name == right.test_name
|
||||
|
||||
|
||||
func test_equals(left: GdUnitTestCase, right: GdUnitTestCase) -> bool:
|
||||
return left.display_name == right.display_name
|
||||
|
||||
|
||||
# do rebuild the entire project, there is actual no way to enforce the Godot engine itself to do this
|
||||
func rebuild_project(script: Script) -> void:
|
||||
var class_path := ProjectSettings.globalize_path(script.resource_path)
|
||||
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard: CSharpScript change detected on: '%s' [/color]" % class_path)
|
||||
var scene_tree := Engine.get_main_loop() as SceneTree
|
||||
await scene_tree.process_frame
|
||||
|
||||
var output := []
|
||||
var exit_code := OS.execute("dotnet", ["--version"], output)
|
||||
if exit_code == -1:
|
||||
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Rebuild the project failed.[/color]")
|
||||
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Can't find installed `dotnet`! Please check your environment is setup correctly.[/color]")
|
||||
return
|
||||
|
||||
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Found dotnet v%s[/color]" % str(output[0]).strip_edges())
|
||||
output.clear()
|
||||
|
||||
exit_code = OS.execute("dotnet", ["build"], output)
|
||||
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Rebuild the project ... [/color]")
|
||||
for out: String in output:
|
||||
print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges())
|
||||
await scene_tree.process_frame
|
||||
0
addons/gdUnit4/src/core/event/GdUnitEvent.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEvent.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEventInit.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEventInit.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEventStop.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitEventStop.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitSessionClose.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitSessionClose.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitSessionStart.gd
Normal file
0
addons/gdUnit4/src/core/event/GdUnitSessionStart.gd
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user