chore(client): add gdUnit4 test framework addon
Vendor gdUnit4 for Godot 4 client testing (D-030). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
## The execution context
|
||||
## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor
|
||||
class_name GdUnitExecutionContext
|
||||
|
||||
enum GC_ORPHANS_CHECK {
|
||||
NONE,
|
||||
SUITE_HOOK_AFTER,
|
||||
TEST_HOOK_AFTER,
|
||||
TEST_CASE
|
||||
}
|
||||
|
||||
|
||||
var _parent_context: GdUnitExecutionContext
|
||||
var _sub_context: Array[GdUnitExecutionContext] = []
|
||||
var _orphan_monitor: GdUnitOrphanNodesMonitor
|
||||
var _memory_observer: GdUnitMemoryObserver
|
||||
var _report_collector: GdUnitTestReportCollector
|
||||
var _timer: LocalTime
|
||||
var _test_case_name: StringName
|
||||
var _test_case_parameter_set: Array
|
||||
var _name: String
|
||||
var _test_execution_iteration: int = 0
|
||||
var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled()
|
||||
var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries()
|
||||
|
||||
|
||||
var error_monitor: GodotGdErrorMonitor = null:
|
||||
get:
|
||||
if _parent_context != null:
|
||||
return _parent_context.error_monitor
|
||||
if error_monitor == null:
|
||||
error_monitor = GodotGdErrorMonitor.new()
|
||||
return error_monitor
|
||||
|
||||
|
||||
var test_suite: GdUnitTestSuite = null:
|
||||
get:
|
||||
if _parent_context != null:
|
||||
return _parent_context.test_suite
|
||||
return test_suite
|
||||
|
||||
|
||||
var test_case: _TestCase = null:
|
||||
get:
|
||||
if test_case == null and _parent_context != null:
|
||||
return _parent_context.test_case
|
||||
return test_case
|
||||
|
||||
|
||||
func _init(name: StringName, parent_context: GdUnitExecutionContext = null) -> void:
|
||||
_name = name
|
||||
_parent_context = parent_context
|
||||
_timer = LocalTime.now()
|
||||
_orphan_monitor = GdUnitOrphanNodesMonitor.new(name)
|
||||
|
||||
if parent_context != null:
|
||||
parent_context._orphan_monitor.add_child_monitor(_orphan_monitor)
|
||||
orphan_monitor_start()
|
||||
|
||||
_memory_observer = GdUnitMemoryObserver.new()
|
||||
_report_collector = GdUnitTestReportCollector.new()
|
||||
if parent_context != null:
|
||||
parent_context._sub_context.append(self)
|
||||
|
||||
|
||||
func dispose() -> void:
|
||||
if test_suite != null:
|
||||
test_suite.free()
|
||||
_timer = null
|
||||
_orphan_monitor = null
|
||||
_report_collector = null
|
||||
_memory_observer = null
|
||||
_parent_context = null
|
||||
test_suite = null
|
||||
test_case = null
|
||||
dispose_sub_contexts()
|
||||
|
||||
|
||||
func dispose_sub_contexts() -> void:
|
||||
for context in _sub_context:
|
||||
context.dispose()
|
||||
_sub_context.clear()
|
||||
|
||||
|
||||
func terminate() -> void:
|
||||
if test_case:
|
||||
test_case.do_terminate()
|
||||
|
||||
|
||||
static func of(pe: GdUnitExecutionContext) -> GdUnitExecutionContext:
|
||||
var context := GdUnitExecutionContext.new(pe._test_case_name, pe)
|
||||
context._test_case_name = pe._test_case_name
|
||||
context._test_execution_iteration = pe._test_execution_iteration
|
||||
return context
|
||||
|
||||
|
||||
static func of_test_case(pe: GdUnitExecutionContext, p_test_case: _TestCase) -> GdUnitExecutionContext:
|
||||
assert(p_test_case, "test_case is null")
|
||||
var context := GdUnitExecutionContext.new(p_test_case.test_name(), pe)
|
||||
context.test_case = p_test_case
|
||||
return context
|
||||
|
||||
|
||||
static func of_parameterized_test(pe: GdUnitExecutionContext, test_case_name: String, test_case_parameter_set: Array) -> GdUnitExecutionContext:
|
||||
var context := GdUnitExecutionContext.new(test_case_name, pe)
|
||||
context._test_case_name = test_case_name
|
||||
context._test_case_parameter_set = test_case_parameter_set
|
||||
return context
|
||||
|
||||
|
||||
func get_test_suite_path() -> String:
|
||||
return test_suite.get_script().resource_path
|
||||
|
||||
|
||||
func get_test_suite_name() -> StringName:
|
||||
return test_suite.get_name()
|
||||
|
||||
|
||||
func get_test_case_name() -> StringName:
|
||||
if _test_case_name.is_empty():
|
||||
return test_case._test_case.display_name
|
||||
return _test_case_name
|
||||
|
||||
|
||||
func error_monitor_start() -> void:
|
||||
error_monitor.start()
|
||||
|
||||
|
||||
func error_monitor_stop() -> void:
|
||||
error_monitor.stop()
|
||||
for error_report in error_monitor.to_reports():
|
||||
if error_report.is_error():
|
||||
_report_collector.push_back(error_report)
|
||||
|
||||
|
||||
func orphan_monitor_start() -> void:
|
||||
_orphan_monitor.start()
|
||||
|
||||
|
||||
func orphan_monitor_collect() -> void:
|
||||
_orphan_monitor.collect()
|
||||
|
||||
|
||||
func orphan_monitor_stop() -> void:
|
||||
_orphan_monitor.stop()
|
||||
|
||||
|
||||
func add_report(report: GdUnitReport) -> GdUnitReport:
|
||||
_report_collector.push_back(report)
|
||||
return report
|
||||
|
||||
|
||||
func reports() -> Array[GdUnitReport]:
|
||||
return _report_collector.reports()
|
||||
|
||||
|
||||
func collect_reports(recursive: bool) -> Array[GdUnitReport]:
|
||||
if not recursive:
|
||||
return reports()
|
||||
|
||||
# we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended`
|
||||
# we strictly need to copy the reports before adding sub context reports to avoid manipulation of the current context
|
||||
var current_reports := reports().duplicate()
|
||||
for sub_context in _sub_context:
|
||||
current_reports.append_array(sub_context.collect_reports(true))
|
||||
|
||||
return current_reports
|
||||
|
||||
|
||||
func calculate_statistics(reports_: Array[GdUnitReport]) -> Dictionary:
|
||||
var failed_count := GdUnitTestReportCollector.count_failures(reports_)
|
||||
var error_count := GdUnitTestReportCollector.count_errors(reports_)
|
||||
var warn_count := GdUnitTestReportCollector.count_warnings(reports_)
|
||||
var skip_count := GdUnitTestReportCollector.count_skipped(reports_)
|
||||
var orphan_count := GdUnitTestReportCollector.count_orphans(reports_)
|
||||
var is_failed := !is_success()
|
||||
var elapsed_time := _timer.elapsed_since_ms()
|
||||
var retries := 1 if _parent_context == null else _sub_context.size()
|
||||
# Mark as flaky if it is successful, but errors were counted
|
||||
var is_flaky := retries > 1 and not is_failed
|
||||
# In the case of a flakiness test, we do not report an error counter, as an unreliable test is considered successful
|
||||
# after a certain number of repetitions.
|
||||
if is_flaky:
|
||||
failed_count = 0
|
||||
|
||||
return {
|
||||
GdUnitEvent.RETRY_COUNT: retries,
|
||||
GdUnitEvent.ELAPSED_TIME: elapsed_time,
|
||||
GdUnitEvent.FAILED: is_failed,
|
||||
GdUnitEvent.ERRORS: error_count > 0,
|
||||
GdUnitEvent.WARNINGS: warn_count > 0,
|
||||
GdUnitEvent.FLAKY: is_flaky,
|
||||
GdUnitEvent.SKIPPED: skip_count > 0,
|
||||
GdUnitEvent.FAILED_COUNT: failed_count,
|
||||
GdUnitEvent.ERROR_COUNT: error_count,
|
||||
GdUnitEvent.SKIPPED_COUNT: skip_count,
|
||||
GdUnitEvent.ORPHAN_NODES: orphan_count,
|
||||
}
|
||||
|
||||
|
||||
func is_success() -> bool:
|
||||
if _sub_context.is_empty():
|
||||
return not _report_collector.has_failures()
|
||||
# we on test suite level?
|
||||
if _parent_context == null:
|
||||
return not _report_collector.has_failures()
|
||||
|
||||
return _sub_context[-1].is_success() and not _report_collector.has_failures()
|
||||
|
||||
|
||||
func is_skipped() -> bool:
|
||||
return (
|
||||
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
|
||||
return c.is_skipped())
|
||||
or test_case.is_skipped() if test_case != null else false
|
||||
)
|
||||
|
||||
|
||||
func is_interupted() -> bool:
|
||||
return false if test_case == null else test_case.is_interupted()
|
||||
|
||||
|
||||
func sum(accum: int, number: int) -> int:
|
||||
return accum + number
|
||||
|
||||
|
||||
func retry_execution() -> bool:
|
||||
var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries
|
||||
if retry:
|
||||
_test_execution_iteration += 1
|
||||
return retry
|
||||
|
||||
|
||||
func register_auto_free(obj: Variant) -> Variant:
|
||||
return _memory_observer.register_auto_free(obj)
|
||||
|
||||
|
||||
## Runs the gdunit garbage collector to free registered object and handle orphan node reporting
|
||||
func gc(gc_orphan_check: GC_ORPHANS_CHECK = GC_ORPHANS_CHECK.NONE) -> void:
|
||||
# unreference last used assert form the test to prevent memory leaks
|
||||
GdUnitThreadManager.get_current_context().clear_assert()
|
||||
await _memory_observer.gc()
|
||||
orphan_monitor_stop()
|
||||
|
||||
match(gc_orphan_check):
|
||||
GC_ORPHANS_CHECK.SUITE_HOOK_AFTER:
|
||||
_orphan_monitor.collect()
|
||||
var orphan_infos := _orphan_monitor.detected_orphans()
|
||||
if orphan_infos.is_empty():
|
||||
return
|
||||
reports().push_front(GdUnitReport.new() \
|
||||
.create(GdUnitReport.ORPHAN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphan_infos))
|
||||
.with_current_value(orphan_infos.size()))
|
||||
|
||||
GC_ORPHANS_CHECK.TEST_HOOK_AFTER:
|
||||
_orphan_monitor.collect()
|
||||
var orphans := _orphan_monitor.detected_orphans()
|
||||
if not orphans.is_empty():
|
||||
reports().push_front(GdUnitReport.new()\
|
||||
.create(GdUnitReport.ORPHAN, 1, GdAssertMessages.orphan_detected_on_test_setup(orphans))
|
||||
.with_current_value(orphans.size()))
|
||||
|
||||
GC_ORPHANS_CHECK.TEST_CASE:
|
||||
var orphans := _orphan_monitor.detected_orphans()
|
||||
if orphans.is_empty():
|
||||
var orphans_count := _orphan_monitor.orphans_count()
|
||||
if orphans_count > 0:
|
||||
reports().push_front(GdUnitReport.new() \
|
||||
.create(GdUnitReport.ORPHAN, test_case.line_number(), GdAssertMessages.orphan_warning(orphans_count))
|
||||
.with_current_value(orphans_count))
|
||||
else:
|
||||
reports().push_front(GdUnitReport.new()\
|
||||
.create(GdUnitReport.ORPHAN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))
|
||||
.with_current_value(orphans.size()))
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6tbrqev7vxuk
|
||||
@@ -0,0 +1,135 @@
|
||||
## The memory watcher for objects that have been registered and are released when 'gc' is called.
|
||||
class_name GdUnitMemoryObserver
|
||||
extends RefCounted
|
||||
|
||||
const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_"
|
||||
const TAG_AUTO_FREE = "GdUnit4_marked_auto_free"
|
||||
const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
|
||||
var _store :Array[Variant] = []
|
||||
# enable for debugging purposes
|
||||
var _is_stdout_verbose := false
|
||||
const _show_debug := false
|
||||
|
||||
|
||||
## Registration of an instance to be released when an execution phase is completed
|
||||
func register_auto_free(obj :Variant) -> Variant:
|
||||
if not is_instance_valid(obj):
|
||||
return obj
|
||||
# do not register on GDScriptNativeClass
|
||||
@warning_ignore("unsafe_cast")
|
||||
if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") :
|
||||
return obj
|
||||
#if obj is GDScript or obj is ScriptExtension:
|
||||
# return obj
|
||||
if obj is MainLoop:
|
||||
push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj)
|
||||
return
|
||||
if _is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():register auto_free(%s)" % obj)
|
||||
# only register pure objects
|
||||
if obj is GdUnitSceneRunner:
|
||||
_store.push_back(obj)
|
||||
else:
|
||||
_store.append(obj)
|
||||
_tag_object(obj)
|
||||
return obj
|
||||
|
||||
|
||||
# to disable instance guard when run into issues.
|
||||
static func _is_instance_guard_enabled() -> bool:
|
||||
return false
|
||||
|
||||
|
||||
static func debug_observe(name :String, obj :Object, indent :int = 0) -> void:
|
||||
if not _show_debug:
|
||||
return
|
||||
var script :GDScript= obj if obj is GDScript else obj.get_script()
|
||||
if script:
|
||||
var base_script :GDScript = script.get_base_script()
|
||||
@warning_ignore("unsafe_method_access")
|
||||
prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path)
|
||||
if base_script:
|
||||
debug_observe("+", base_script, indent+1)
|
||||
else:
|
||||
@warning_ignore("unsafe_method_access")
|
||||
prints(name, obj, obj.get_class(), obj.get_name())
|
||||
|
||||
|
||||
static func guard_instance(obj :Object) -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
|
||||
if Engine.has_meta(tag):
|
||||
return
|
||||
debug_observe("Gard on instance", obj)
|
||||
Engine.set_meta(tag, obj)
|
||||
|
||||
|
||||
static func unguard_instance(obj :Object, verbose := true) -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
|
||||
if verbose:
|
||||
debug_observe("unguard instance", obj)
|
||||
if Engine.has_meta(tag):
|
||||
Engine.remove_meta(tag)
|
||||
|
||||
|
||||
static func gc_guarded_instance(name :String, instance :Object) -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
unguard_instance(instance, false)
|
||||
if is_instance_valid(instance) and instance is RefCounted:
|
||||
# finally do this very hacky stuff
|
||||
# we need to manually unreferece to avoid leaked scripts
|
||||
# but still leaked GDScriptFunctionState exists
|
||||
#var script :GDScript = instance.get_script()
|
||||
#if script:
|
||||
# var base_script :GDScript = script.get_base_script()
|
||||
# if base_script:
|
||||
# base_script.unreference()
|
||||
debug_observe(name, instance)
|
||||
(instance as RefCounted).unreference()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
|
||||
|
||||
static func gc_on_guarded_instances() -> void:
|
||||
if not _is_instance_guard_enabled():
|
||||
return
|
||||
for tag in Engine.get_meta_list():
|
||||
if tag.begins_with(TAG_OBSERVE_INSTANCE):
|
||||
var instance :Object = Engine.get_meta(tag)
|
||||
await gc_guarded_instance("Leaked instance detected:", instance)
|
||||
await GdUnitTools.free_instance(instance, false)
|
||||
|
||||
|
||||
# store the object into global store aswell to be verified by 'is_marked_auto_free'
|
||||
func _tag_object(obj :Variant) -> void:
|
||||
var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, [])
|
||||
tagged_object.append(obj)
|
||||
Engine.set_meta(TAG_AUTO_FREE, tagged_object)
|
||||
|
||||
|
||||
## Runs over all registered objects and releases them
|
||||
func gc() -> void:
|
||||
if _store.is_empty():
|
||||
return
|
||||
# give engine time to free objects to process objects marked by queue_free()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
if _is_stdout_verbose:
|
||||
print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size())
|
||||
var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
|
||||
while not _store.is_empty():
|
||||
var value :Variant = _store.pop_front()
|
||||
tagged_objects.erase(value)
|
||||
await GdUnitTools.free_instance(value, _is_stdout_verbose)
|
||||
assert(_store.is_empty(), "The memory observer has still entries in the store!")
|
||||
|
||||
|
||||
## Checks whether the specified object is registered for automatic release
|
||||
static func is_marked_auto_free(obj: Variant) -> bool:
|
||||
var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
|
||||
return tagged_objects.has(obj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://beik6mbe7oahv
|
||||
@@ -0,0 +1,77 @@
|
||||
# Collects all reports seperated as warnings, failures and errors
|
||||
class_name GdUnitTestReportCollector
|
||||
extends RefCounted
|
||||
|
||||
|
||||
var _reports :Array[GdUnitReport] = []
|
||||
|
||||
|
||||
static func __filter_is_error(report :GdUnitReport) -> bool:
|
||||
return report.is_error()
|
||||
|
||||
|
||||
static func __filter_is_failure(report :GdUnitReport) -> bool:
|
||||
return report.is_failure()
|
||||
|
||||
|
||||
static func __filter_is_warning(report :GdUnitReport) -> bool:
|
||||
return report.is_warning()
|
||||
|
||||
|
||||
static func __filter_is_skipped(report :GdUnitReport) -> bool:
|
||||
return report.is_skipped()
|
||||
|
||||
|
||||
static func __filter_is_orphan(report: GdUnitReport) -> bool:
|
||||
return report.is_orphan()
|
||||
|
||||
|
||||
static func count_failures(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_failure).size()
|
||||
|
||||
|
||||
static func count_errors(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_error).size()
|
||||
|
||||
|
||||
static func count_warnings(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_warning).size()
|
||||
|
||||
|
||||
static func count_skipped(reports_: Array[GdUnitReport]) -> int:
|
||||
return reports_.filter(__filter_is_skipped).size()
|
||||
|
||||
|
||||
static func count_orphans(reports_: Array[GdUnitReport]) -> int:
|
||||
var orphan_reports := reports_.filter(__filter_is_orphan)
|
||||
if orphan_reports.is_empty():
|
||||
return 0
|
||||
## Collect orphan count from the reports
|
||||
var orphans := 0
|
||||
for report: GdUnitReport in orphan_reports:
|
||||
orphans += report._current_value
|
||||
return orphans
|
||||
|
||||
|
||||
func has_failures() -> bool:
|
||||
return _reports.any(__filter_is_failure)
|
||||
|
||||
|
||||
func has_errors() -> bool:
|
||||
return _reports.any(__filter_is_error)
|
||||
|
||||
|
||||
func has_warnings() -> bool:
|
||||
return _reports.any(__filter_is_warning)
|
||||
|
||||
|
||||
func has_skipped() -> bool:
|
||||
return _reports.any(__filter_is_skipped)
|
||||
|
||||
|
||||
func reports() -> Array[GdUnitReport]:
|
||||
return _reports
|
||||
|
||||
|
||||
func push_back(report :GdUnitReport) -> void:
|
||||
_reports.push_back(report)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwi34h40wy72u
|
||||
@@ -0,0 +1,56 @@
|
||||
## The executor to run a test-suite
|
||||
class_name GdUnitTestSuiteExecutor
|
||||
|
||||
|
||||
# preload all asserts here
|
||||
@warning_ignore("unused_private_class_variable")
|
||||
var _assertions := GdUnitAssertions.new()
|
||||
var _executeStage := GdUnitTestSuiteExecutionStage.new()
|
||||
var _debug_mode : bool
|
||||
var _terminated := false
|
||||
|
||||
func _init(debug_mode: bool = false) -> void:
|
||||
_executeStage.set_debug_mode(debug_mode)
|
||||
_debug_mode = debug_mode
|
||||
GdUnitSignals.instance().gdunit_test_session_terminate.connect(_on_testsession_terminated)
|
||||
|
||||
|
||||
func _on_testsession_terminated() -> void:
|
||||
_terminated = true
|
||||
GdUnitThreadManager.interrupt()
|
||||
|
||||
|
||||
func run_and_wait(tests: Array[GdUnitTestCase]) -> void:
|
||||
if !_debug_mode:
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitInit.new())
|
||||
|
||||
var orphan_detection_enabled := GdUnitSettings.is_verbose_orphans()
|
||||
if not orphan_detection_enabled:
|
||||
prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.")
|
||||
|
||||
# first we group all tests by resource path
|
||||
var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String:
|
||||
return test.suite_resource_path
|
||||
)
|
||||
var scanner := GdUnitTestSuiteScanner.new()
|
||||
for suite_path: String in grouped_by_suites.keys():
|
||||
if _terminated:
|
||||
break
|
||||
@warning_ignore("unsafe_call_argument")
|
||||
var suite_tests: Array[GdUnitTestCase] = Array(grouped_by_suites[suite_path], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
|
||||
var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(suite_path)
|
||||
if script.get_class() == "GDScript":
|
||||
var context := GdUnitExecutionContext.new(suite_path)
|
||||
var test_suite := scanner.load_suite(script as GDScript, suite_tests)
|
||||
context.test_suite = test_suite
|
||||
(Engine.get_main_loop() as SceneTree).root.add_child(test_suite)
|
||||
await _executeStage.execute(context)
|
||||
context.dispose()
|
||||
else:
|
||||
await GdUnit4CSharpApiLoader.execute(suite_tests)
|
||||
if !_debug_mode:
|
||||
GdUnitSignals.instance().gdunit_event.emit(GdUnitStop.new())
|
||||
|
||||
|
||||
func fail_fast(enabled :bool) -> void:
|
||||
_executeStage.fail_fast(enabled)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxinlamg7ka6s
|
||||
@@ -0,0 +1,22 @@
|
||||
## The test case shutdown hook implementation.[br]
|
||||
## It executes the 'test_after()' block from the test-suite.
|
||||
class_name GdUnitTestCaseAfterStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
var _call_stage: bool
|
||||
|
||||
|
||||
func _init(call_stage := true) -> void:
|
||||
_call_stage = call_stage
|
||||
|
||||
|
||||
func _execute(context: GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
if _call_stage:
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.after_test()
|
||||
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER)
|
||||
context.error_monitor_stop()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0i7ud0nko07o
|
||||
@@ -0,0 +1,20 @@
|
||||
## The test case startup hook implementation.[br]
|
||||
## It executes the 'test_before()' block from the test-suite.
|
||||
class_name GdUnitTestCaseBeforeStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
var _call_stage :bool
|
||||
|
||||
|
||||
func _init(call_stage := true) -> void:
|
||||
_call_stage = call_stage
|
||||
|
||||
|
||||
func _execute(context: GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
if _call_stage:
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.before_test()
|
||||
|
||||
context.error_monitor_start()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cx8mwagqugp5y
|
||||
@@ -0,0 +1,37 @@
|
||||
## The test case execution stage.[br]
|
||||
class_name GdUnitTestCaseExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
var _stage_single_test: IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new()
|
||||
var _stage_fuzzer_test: IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new()
|
||||
|
||||
|
||||
## Executes the test case 'test_<name>()'.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> test_before() [br]
|
||||
## -> test_case() [br]
|
||||
## -> test_after() [br]
|
||||
@warning_ignore("redundant_await")
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_case := context.test_case
|
||||
|
||||
context.error_monitor_start()
|
||||
|
||||
if test_case.is_fuzzed():
|
||||
await _stage_fuzzer_test.execute(context)
|
||||
else:
|
||||
await _stage_single_test.execute(context)
|
||||
|
||||
await context.gc()
|
||||
context.error_monitor_stop()
|
||||
|
||||
# finally free the test instance
|
||||
if is_instance_valid(context.test_case):
|
||||
context.test_case.dispose()
|
||||
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_single_test.set_debug_mode(debug_mode)
|
||||
_stage_fuzzer_test.set_debug_mode(debug_mode)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ci15c4tn1gld4
|
||||
@@ -0,0 +1,29 @@
|
||||
## The test suite shutdown hook implementation.[br]
|
||||
## It executes the 'after()' block from the test-suite.
|
||||
class_name GdUnitTestSuiteAfterStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.after()
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER)
|
||||
|
||||
var reports := context.collect_reports(false)
|
||||
var statistics := context.calculate_statistics(reports)
|
||||
fire_event(GdUnitEvent.new()\
|
||||
.suite_after(context.get_test_suite_path(),\
|
||||
test_suite.get_name(),
|
||||
statistics,
|
||||
reports))
|
||||
GdUnitFileAccess.clear_tmp()
|
||||
# Guard that checks if all doubled (spy/mock) objects are released
|
||||
await GdUnitClassDoubler.check_leaked_instances()
|
||||
# we hide the scene/main window after runner is finished
|
||||
if not Engine.is_embedded_in_editor():
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcj0n64icx10i
|
||||
@@ -0,0 +1,14 @@
|
||||
## The test suite startup hook implementation.[br]
|
||||
## It executes the 'before()' block from the test-suite.
|
||||
class_name GdUnitTestSuiteBeforeStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
func _execute(context: GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
|
||||
fire_event(GdUnitEvent.new()\
|
||||
.suite_before(context.get_test_suite_path(), test_suite.get_name(), test_suite.get_child_count()))
|
||||
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.before()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjudpgdgtetuf
|
||||
@@ -0,0 +1,147 @@
|
||||
## The test suite main execution stage.[br]
|
||||
class_name GdUnitTestSuiteExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
|
||||
|
||||
var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new()
|
||||
var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new()
|
||||
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new()
|
||||
var _fail_fast := false
|
||||
|
||||
|
||||
## Executes all tests of an test suite.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> before() [br]
|
||||
## -> run all test cases [br]
|
||||
## -> after() [br]
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
if context.test_suite.__is_skipped:
|
||||
await fire_test_suite_skipped(context)
|
||||
else:
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter)
|
||||
await _stage_before.execute(context)
|
||||
for test_case_index in context.test_suite.get_child_count():
|
||||
# iterate only over test cases
|
||||
var test_case := context.test_suite.get_child(test_case_index) as _TestCase
|
||||
if not is_instance_valid(test_case):
|
||||
continue
|
||||
context.test_suite.set_active_test_case(test_case.test_name())
|
||||
var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case)
|
||||
await _stage_test.execute(test_case_context)
|
||||
# stop on first error or if fail fast is enabled
|
||||
if test_case.is_terminated() or (_fail_fast and not test_case_context.is_success()):
|
||||
break
|
||||
if test_case.is_interupted():
|
||||
# it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out
|
||||
# we delete the current test suite where is execute the current test case to kill the function state
|
||||
# and replace it by a clone without function state
|
||||
context.test_suite = await clone_test_suite(context.test_suite)
|
||||
await _stage_after.execute(context)
|
||||
GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter)
|
||||
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
|
||||
|
||||
# clones a test suite and moves the test cases to new instance
|
||||
func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite:
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
dispose_timers(test_suite)
|
||||
await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter)
|
||||
var parent := test_suite.get_parent()
|
||||
var _test_suite := GdUnitTestSuite.new()
|
||||
parent.remove_child(test_suite)
|
||||
copy_properties(test_suite, _test_suite)
|
||||
for child in test_suite.get_children():
|
||||
test_suite.remove_child(child)
|
||||
_test_suite.add_child(child)
|
||||
parent.add_child(_test_suite)
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter)
|
||||
# finally free current test suite instance
|
||||
test_suite.free()
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
return _test_suite
|
||||
|
||||
|
||||
func dispose_timers(test_suite :GdUnitTestSuite) -> void:
|
||||
GdUnitTools.release_timers()
|
||||
for child in test_suite.get_children():
|
||||
if child is Timer:
|
||||
(child as Timer).stop()
|
||||
test_suite.remove_child(child)
|
||||
child.free()
|
||||
|
||||
|
||||
func copy_properties(source :Object, target :Object) -> void:
|
||||
if not source is _TestCase and not source is GdUnitTestSuite:
|
||||
return
|
||||
for property in source.get_property_list():
|
||||
var property_name :String = property["name"]
|
||||
if property_name == "__awaiter":
|
||||
continue
|
||||
target.set(property_name, source.get(property_name))
|
||||
|
||||
|
||||
func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
var skip_count := test_suite.get_child_count()
|
||||
fire_event(GdUnitEvent.new()\
|
||||
.suite_before(context.get_test_suite_path(), test_suite.get_name(), skip_count))
|
||||
|
||||
|
||||
for test_case_index in context.test_suite.get_child_count():
|
||||
# iterate only over test cases
|
||||
var test_case := context.test_suite.get_child(test_case_index) as _TestCase
|
||||
if not is_instance_valid(test_case):
|
||||
continue
|
||||
var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case)
|
||||
fire_event(GdUnitEvent.new().test_before(test_case.id()))
|
||||
# use skip count 0 because we counted it over the complete test suite
|
||||
fire_test_skipped(test_case_context, 0)
|
||||
|
||||
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED_COUNT: skip_count,
|
||||
GdUnitEvent.SKIPPED: true
|
||||
}
|
||||
var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count))
|
||||
fire_event(GdUnitEvent.new().suite_after(context.get_test_suite_path(), test_suite.get_name(), statistics, [report]))
|
||||
await (Engine.get_main_loop() as SceneTree).process_frame
|
||||
|
||||
|
||||
func fire_test_skipped(context: GdUnitExecutionContext, skip_count := 1) -> void:
|
||||
var test_case := context.test_case
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED: true,
|
||||
GdUnitEvent.SKIPPED_COUNT: skip_count,
|
||||
}
|
||||
var report := GdUnitReport.new() \
|
||||
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped("Skipped from the entire test suite"))
|
||||
fire_event(GdUnitEvent.new().test_after(test_case.id(), test_case.test_name(), statistics, [report]))
|
||||
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_before.set_debug_mode(debug_mode)
|
||||
_stage_after.set_debug_mode(debug_mode)
|
||||
_stage_test.set_debug_mode(debug_mode)
|
||||
|
||||
|
||||
func fail_fast(enabled :bool) -> void:
|
||||
_fail_fast = enabled
|
||||
@@ -0,0 +1 @@
|
||||
uid://btrvyvk62pxxd
|
||||
@@ -0,0 +1,37 @@
|
||||
## The interface of execution stage.[br]
|
||||
## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br]
|
||||
## Execution stage are always called synchronously.
|
||||
@abstract class_name IGdUnitExecutionStage
|
||||
extends RefCounted
|
||||
|
||||
var _debug_mode := false
|
||||
|
||||
|
||||
## Executes synchronized the implemented stage in its own execution context.[br]
|
||||
## example:[br]
|
||||
## [codeblock]
|
||||
## # waits for 100ms
|
||||
## await MyExecutionStage.new().execute(<GdUnitExecutionContext>)
|
||||
## [/codeblock][br]
|
||||
func execute(context: GdUnitExecutionContext) -> void:
|
||||
GdUnitThreadManager.get_current_context().set_execution_context(context)
|
||||
@warning_ignore("redundant_await")
|
||||
await _execute(context)
|
||||
|
||||
|
||||
## Sends the event to registered listeners
|
||||
func fire_event(event: GdUnitEvent) -> void:
|
||||
if _debug_mode:
|
||||
GdUnitSignals.instance().gdunit_event_debug.emit(event)
|
||||
else:
|
||||
GdUnitSignals.instance().gdunit_event.emit(event)
|
||||
|
||||
|
||||
## Internal testing stuff.[br]
|
||||
## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug`
|
||||
func set_debug_mode(debug_mode: bool) -> void:
|
||||
_debug_mode = debug_mode
|
||||
|
||||
|
||||
## The execution phase to be carried out.
|
||||
@abstract func _execute(context: GdUnitExecutionContext) -> void
|
||||
@@ -0,0 +1 @@
|
||||
uid://cvtdudovarffs
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
## The test case execution stage.[br]
|
||||
class_name GdUnitTestCaseFuzzedExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false)
|
||||
var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false)
|
||||
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new()
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
fire_event(GdUnitEvent.new().test_before(context.test_case.id()))
|
||||
|
||||
while context.retry_execution():
|
||||
var test_context := GdUnitExecutionContext.of(context)
|
||||
await _stage_before.execute(test_context)
|
||||
if not context.test_case.is_skipped():
|
||||
await _stage_test.execute(GdUnitExecutionContext.of(test_context))
|
||||
await _stage_after.execute(test_context)
|
||||
if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted():
|
||||
break
|
||||
|
||||
context.gc()
|
||||
if context.is_skipped():
|
||||
fire_test_skipped(context)
|
||||
else:
|
||||
var reports: = context.collect_reports(true)
|
||||
var statistics := context.calculate_statistics(reports)
|
||||
fire_event(GdUnitEvent.new().test_after(context.test_case.id(), context.test_case.test_name(), statistics, reports))
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_before.set_debug_mode(debug_mode)
|
||||
_stage_after.set_debug_mode(debug_mode)
|
||||
_stage_test.set_debug_mode(debug_mode)
|
||||
|
||||
|
||||
func fire_test_skipped(context: GdUnitExecutionContext) -> void:
|
||||
var test_case := context.test_case
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED: true,
|
||||
GdUnitEvent.SKIPPED_COUNT: 1,
|
||||
}
|
||||
var report := GdUnitReport.new() \
|
||||
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info()))
|
||||
fire_event(GdUnitEvent.new().test_after(test_case.id(), test_case.test_name(), statistics, [report]))
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://dbwwgjmaop4sw
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
## The fuzzed test case execution stage.[br]
|
||||
class_name GdUnitTestCaseFuzzedTestStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
var _expression_runner := GdUnitExpressionRunner.new()
|
||||
|
||||
|
||||
## Executes a test case with given fuzzers 'test_<name>(<fuzzer>)' iterative.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> test_case() [br]
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
var test_suite := context.test_suite
|
||||
var test_case := context.test_case
|
||||
var fuzzers := create_fuzzers(test_suite, test_case)
|
||||
|
||||
# guard on fuzzers
|
||||
for fuzzer in fuzzers:
|
||||
@warning_ignore("return_value_discarded")
|
||||
GdUnitMemoryObserver.guard_instance(fuzzer)
|
||||
|
||||
for iteration in test_case.iterations():
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.before_test()
|
||||
await test_case.execute(fuzzers, iteration)
|
||||
@warning_ignore("redundant_await")
|
||||
await test_suite.after_test()
|
||||
if test_case.is_interupted():
|
||||
break
|
||||
# interrupt at first failure
|
||||
var reports := context.reports()
|
||||
if not reports.is_empty():
|
||||
var report :GdUnitReport = reports.pop_front()
|
||||
reports.append(GdUnitReport.new() \
|
||||
.create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message())))
|
||||
break
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE)
|
||||
|
||||
# unguard on fuzzers
|
||||
if not test_case.is_interupted():
|
||||
for fuzzer in fuzzers:
|
||||
GdUnitMemoryObserver.unguard_instance(fuzzer)
|
||||
|
||||
|
||||
func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]:
|
||||
if not test_case.is_fuzzed():
|
||||
return Array()
|
||||
test_case.generate_seed()
|
||||
var fuzzers :Array[Fuzzer] = []
|
||||
for fuzzer_arg in test_case.fuzzer_arguments():
|
||||
@warning_ignore("unsafe_cast")
|
||||
var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script() as GDScript, fuzzer_arg.plain_value() as String)
|
||||
fuzzer._iteration_index = 0
|
||||
fuzzer._iteration_limit = test_case.iterations()
|
||||
fuzzers.append(fuzzer)
|
||||
return fuzzers
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://cuh0b47yf68tg
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
## The test case execution stage.[br]
|
||||
class_name GdUnitTestCaseSingleExecutionStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new()
|
||||
var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new()
|
||||
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new()
|
||||
|
||||
|
||||
func _execute(context :GdUnitExecutionContext) -> void:
|
||||
fire_event(GdUnitEvent.new().test_before(context.test_case.id()))
|
||||
while context.retry_execution():
|
||||
var test_context := GdUnitExecutionContext.of(context)
|
||||
await _stage_before.execute(test_context)
|
||||
if not test_context.is_skipped():
|
||||
await _stage_test.execute(GdUnitExecutionContext.of(test_context))
|
||||
await _stage_after.execute(test_context)
|
||||
if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted():
|
||||
break
|
||||
|
||||
context.gc()
|
||||
if context.is_skipped():
|
||||
fire_test_skipped(context)
|
||||
else:
|
||||
var reports: = context.collect_reports(true)
|
||||
var statistics := context.calculate_statistics(reports)
|
||||
fire_event(GdUnitEvent.new().test_after(context.test_case.id(), context.test_case.test_name(), statistics, reports))
|
||||
|
||||
|
||||
func set_debug_mode(debug_mode :bool = false) -> void:
|
||||
super.set_debug_mode(debug_mode)
|
||||
_stage_before.set_debug_mode(debug_mode)
|
||||
_stage_after.set_debug_mode(debug_mode)
|
||||
_stage_test.set_debug_mode(debug_mode)
|
||||
|
||||
|
||||
func fire_test_skipped(context: GdUnitExecutionContext) -> void:
|
||||
var test_case := context.test_case
|
||||
var statistics := {
|
||||
GdUnitEvent.ORPHAN_NODES: 0,
|
||||
GdUnitEvent.ELAPSED_TIME: 0,
|
||||
GdUnitEvent.WARNINGS: false,
|
||||
GdUnitEvent.ERRORS: false,
|
||||
GdUnitEvent.ERROR_COUNT: 0,
|
||||
GdUnitEvent.FAILED: false,
|
||||
GdUnitEvent.FAILED_COUNT: 0,
|
||||
GdUnitEvent.SKIPPED: true,
|
||||
GdUnitEvent.SKIPPED_COUNT: 1,
|
||||
}
|
||||
var report := GdUnitReport.new() \
|
||||
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info()))
|
||||
fire_event(GdUnitEvent.new().test_after(test_case.id(), test_case.test_name(), statistics, [report]))
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://ccnmusbucrtam
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
## The single test case execution stage.[br]
|
||||
class_name GdUnitTestCaseSingleTestStage
|
||||
extends IGdUnitExecutionStage
|
||||
|
||||
|
||||
## Executes a single test case 'test_<name>()'.[br]
|
||||
## It executes synchronized following stages[br]
|
||||
## -> test_case() [br]
|
||||
func _execute(context: GdUnitExecutionContext) -> void:
|
||||
await context.test_case.execute()
|
||||
await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE)
|
||||
+1
@@ -0,0 +1 @@
|
||||
uid://cjyply33fa4pj
|
||||
Reference in New Issue
Block a user