summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/core/execution
diff options
context:
space:
mode:
authorradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
committerradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
commita89e744ed0e8c759caf041a580112365bbfe4303 (patch)
treedd61e320904176995727e86a857d00c90d4933ba /addons/gdUnit4/src/core/execution
parent6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff)
escoria updateHEADmain
Diffstat (limited to 'addons/gdUnit4/src/core/execution')
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd274
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd135
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd77
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd56
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd22
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd20
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd37
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd29
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd14
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd147
-rw-r--r--addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd37
-rw-r--r--addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd52
-rw-r--r--addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd55
-rw-r--r--addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd53
-rw-r--r--addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid1
-rw-r--r--addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd11
-rw-r--r--addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid1
30 files changed, 1034 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd
new file mode 100644
index 0000000..844561b
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd
@@ -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()))
diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid
new file mode 100644
index 0000000..98ede87
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid
@@ -0,0 +1 @@
+uid://doxksl3xmwd4t
diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
new file mode 100644
index 0000000..dd03a31
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
@@ -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)
diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid
new file mode 100644
index 0000000..35465f8
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid
@@ -0,0 +1 @@
+uid://3kyvv6qv76xy
diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd
new file mode 100644
index 0000000..91247bd
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd
@@ -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)
diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid
new file mode 100644
index 0000000..3c1601f
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid
@@ -0,0 +1 @@
+uid://5dmb52xvknlt
diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd
new file mode 100644
index 0000000..dfb4441
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd
@@ -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)
diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid
new file mode 100644
index 0000000..6717eba
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid
@@ -0,0 +1 @@
+uid://cfluc4qh1cia6
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd
new file mode 100644
index 0000000..f106f21
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd
@@ -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()
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid
new file mode 100644
index 0000000..f241c9f
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid
@@ -0,0 +1 @@
+uid://cry2ikys4ww2x
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd
new file mode 100644
index 0000000..d7790a9
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd
@@ -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()
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid
new file mode 100644
index 0000000..2073bc3
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid
@@ -0,0 +1 @@
+uid://lqofqnr401s3
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd
new file mode 100644
index 0000000..473f1e2
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd
@@ -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)
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid
new file mode 100644
index 0000000..7105242
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid
@@ -0,0 +1 @@
+uid://bnx6cbvnp5pr1
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd
new file mode 100644
index 0000000..03bbd0f
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd
@@ -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)
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid
new file mode 100644
index 0000000..1b19c2a
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid
@@ -0,0 +1 @@
+uid://mwtvw184h4nk
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd
new file mode 100644
index 0000000..a6ed8c5
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd
@@ -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()
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid
new file mode 100644
index 0000000..2fdfdc3
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid
@@ -0,0 +1 @@
+uid://fx62snxk32g4
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd
new file mode 100644
index 0000000..07e3779
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd
@@ -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
diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid
new file mode 100644
index 0000000..f250acb
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid
@@ -0,0 +1 @@
+uid://c00afkws3ewfx
diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd
new file mode 100644
index 0000000..a04f7e7
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd
@@ -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
diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid
new file mode 100644
index 0000000..5ae6f69
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid
@@ -0,0 +1 @@
+uid://bfvoxsa3oad2k
diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd
new file mode 100644
index 0000000..c1e0924
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd
@@ -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]))
diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid
new file mode 100644
index 0000000..7444e55
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid
@@ -0,0 +1 @@
+uid://buqq7wmo7x6de
diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd
new file mode 100644
index 0000000..62dda37
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd
@@ -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
diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid
new file mode 100644
index 0000000..5df0ddb
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid
@@ -0,0 +1 @@
+uid://b3tpfwvmfjkoq
diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd
new file mode 100644
index 0000000..03f8fa5
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd
@@ -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]))
diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid
new file mode 100644
index 0000000..9a0459d
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid
@@ -0,0 +1 @@
+uid://ctjx74gak008y
diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd
new file mode 100644
index 0000000..b8ace50
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd
@@ -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)
diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid
new file mode 100644
index 0000000..48f2c97
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid
@@ -0,0 +1 @@
+uid://66t5ejuybo1s