Adding log.gd

This commit is contained in:
Dan Baker 2024-05-02 09:36:31 +01:00
parent eb32d6614e
commit 4522259397
547 changed files with 46844 additions and 0 deletions

View file

@ -0,0 +1,192 @@
## The execution context
## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor
class_name GdUnitExecutionContext
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 _name :String
var error_monitor : GodotGdErrorMonitor = null:
set (value):
error_monitor = value
get:
if _parent_context != null:
return _parent_context.error_monitor
return error_monitor
var test_suite : GdUnitTestSuite = null:
set (value):
test_suite = value
get:
if _parent_context != null:
return _parent_context.test_suite
return test_suite
var test_case : _TestCase = null:
get:
if _test_case_name.is_empty():
return null
return test_suite.find_child(_test_case_name, false, false)
func _init(name :String, parent_context :GdUnitExecutionContext = null) -> void:
_name = name
_parent_context = parent_context
_timer = LocalTime.now()
_orphan_monitor = GdUnitOrphanNodesMonitor.new(name)
_orphan_monitor.start()
_memory_observer = GdUnitMemoryObserver.new()
error_monitor = GodotGdErrorMonitor.new()
_report_collector = GdUnitTestReportCollector.new(get_instance_id())
if parent_context != null:
parent_context._sub_context.append(self)
func dispose() -> void:
_timer = null
_orphan_monitor = null
_report_collector = null
_memory_observer = null
_parent_context = null
test_suite = null
test_case = null
for context in _sub_context:
context.dispose()
_sub_context.clear()
func set_active() -> void:
test_suite.__execution_context = self
GdUnitThreadManager.get_current_context().set_execution_context(self)
static func of_test_suite(test_suite_ :GdUnitTestSuite) -> GdUnitExecutionContext:
assert(test_suite_, "test_suite is null")
var context := GdUnitExecutionContext.new(test_suite_.get_name())
context.test_suite = test_suite_
context.set_active()
return context
static func of_test_case(pe :GdUnitExecutionContext, test_case_name :StringName) -> GdUnitExecutionContext:
var context := GdUnitExecutionContext.new(test_case_name, pe)
context._test_case_name = test_case_name
context.set_active()
return context
static func of(pe :GdUnitExecutionContext) -> GdUnitExecutionContext:
var context := GdUnitExecutionContext.new(pe._test_case_name, pe)
context._test_case_name = pe._test_case_name
context.set_active()
return context
func test_failed() -> bool:
return has_failures() or has_errors()
func error_monitor_start() -> void:
error_monitor.start()
func error_monitor_stop() -> void:
await error_monitor.scan()
for error_report in error_monitor.to_reports():
if error_report.is_error():
_report_collector._reports.append(error_report)
func orphan_monitor_start() -> void:
_orphan_monitor.start()
func orphan_monitor_stop() -> void:
_orphan_monitor.stop()
func reports() -> Array[GdUnitReport]:
return _report_collector.reports()
func build_report_statistics(orphans :int, recursive := true) -> Dictionary:
return {
GdUnitEvent.ORPHAN_NODES: orphans,
GdUnitEvent.ELAPSED_TIME: _timer.elapsed_since_ms(),
GdUnitEvent.FAILED: has_failures(),
GdUnitEvent.ERRORS: has_errors(),
GdUnitEvent.WARNINGS: has_warnings(),
GdUnitEvent.SKIPPED: has_skipped(),
GdUnitEvent.FAILED_COUNT: count_failures(recursive),
GdUnitEvent.ERROR_COUNT: count_errors(recursive),
GdUnitEvent.SKIPPED_COUNT: count_skipped(recursive)
}
func has_failures() -> bool:
return _sub_context.any(func(c): return c.has_failures()) or _report_collector.has_failures()
func has_errors() -> bool:
return _sub_context.any(func(c): return c.has_errors()) or _report_collector.has_errors()
func has_warnings() -> bool:
return _sub_context.any(func(c): return c.has_warnings()) or _report_collector.has_warnings()
func has_skipped() -> bool:
return _sub_context.any(func(c): return c.has_skipped()) or _report_collector.has_skipped()
func count_failures(recursive :bool) -> int:
if not recursive:
return _report_collector.count_failures()
return _sub_context\
.map(func(c): return c.count_failures(recursive))\
.reduce(sum, _report_collector.count_failures())
func count_errors(recursive :bool) -> int:
if not recursive:
return _report_collector.count_errors()
return _sub_context\
.map(func(c): return c.count_errors(recursive))\
.reduce(sum, _report_collector.count_errors())
func count_skipped(recursive :bool) -> int:
if not recursive:
return _report_collector.count_skipped()
return _sub_context\
.map(func(c): return c.count_skipped(recursive))\
.reduce(sum, _report_collector.count_skipped())
func count_orphans() -> int:
var orphans := 0
for c in _sub_context:
orphans += c._orphan_monitor.orphan_nodes()
return _orphan_monitor.orphan_nodes() - orphans
func sum(accum :int, number :int) -> int:
return accum + number
func register_auto_free(obj :Variant) -> Variant:
return _memory_observer.register_auto_free(obj)
## Runs the gdunit garbage collector to free registered object
func gc() -> void:
await _memory_observer.gc()
orphan_monitor_stop()

View file

@ -0,0 +1,131 @@
## 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:
if not is_instance_valid(obj):
return obj
# do not register on GDScriptNativeClass
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()
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:
prints(name, obj, obj.get_class(), obj.get_name())
static func guard_instance(obj :Object) -> Object:
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)
return 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().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.unreference()
await Engine.get_main_loop().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 = 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 := Engine.get_meta(TAG_AUTO_FREE, []) as Array
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().process_frame
if _is_stdout_verbose:
print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size())
var tagged_objects := Engine.get_meta(TAG_AUTO_FREE, []) as Array
while not _store.is_empty():
var value :Variant = _store.pop_front()
tagged_objects.erase(value)
await GdUnitTools.free_instance(value, _is_stdout_verbose)
## Checks whether the specified object is registered for automatic release
static func is_marked_auto_free(obj) -> bool:
return Engine.get_meta(TAG_AUTO_FREE, []).has(obj)

View file

@ -0,0 +1,70 @@
# Collects all reports seperated as warnings, failures and errors
class_name GdUnitTestReportCollector
extends RefCounted
var _execution_context_id :int
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()
func _init(execution_context_id :int):
_execution_context_id = execution_context_id
GdUnitSignals.instance().gdunit_report.connect(on_reports)
func count_failures() -> int:
return _reports.filter(__filter_is_failure).size()
func count_errors() -> int:
return _reports.filter(__filter_is_error).size()
func count_warnings() -> int:
return _reports.filter(__filter_is_warning).size()
func count_skipped() -> int:
return _reports.filter(__filter_is_skipped).size()
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
# Consumes reports emitted by tests
func on_reports(execution_context_id :int, report :GdUnitReport) -> void:
if execution_context_id == _execution_context_id:
_reports.append(report)

View file

@ -0,0 +1,26 @@
## 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 :IGdUnitExecutionStage = GdUnitTestSuiteExecutionStage.new()
func _init(debug_mode :bool = false) -> void:
_executeStage.set_debug_mode(debug_mode)
func execute(test_suite :GdUnitTestSuite) -> void:
var orphan_detection_enabled := GdUnitSettings.is_verbose_orphans()
if not orphan_detection_enabled:
prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.")
Engine.get_main_loop().root.call_deferred("add_child", test_suite)
await Engine.get_main_loop().process_frame
await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite))
func fail_fast(enabled :bool) -> void:
_executeStage.fail_fast(enabled)

View file

@ -0,0 +1,100 @@
## The test case shutdown hook implementation.[br]
## It executes the 'test_after()' block from the test-suite.
class_name GdUnitTestCaseAfterStage
extends IGdUnitExecutionStage
var _test_name :StringName = ""
var _call_stage :bool
func _init(call_stage := true):
_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()
# unreference last used assert form the test to prevent memory leaks
GdUnitThreadManager.get_current_context().set_assert(null)
await context.gc()
await context.error_monitor_stop()
if context.test_case.is_skipped():
fire_test_skipped(context)
else:
fire_test_ended(context)
if is_instance_valid(context.test_case):
context.test_case.dispose()
func set_test_name(test_name :StringName):
_test_name = test_name
func fire_test_ended(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
var test_name := context._test_case_name if _test_name.is_empty() else _test_name
var reports := collect_reports(context)
var orphans := collect_orphans(context, reports)
fire_event(GdUnitEvent.new()\
.test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_name, context.build_report_statistics(orphans), reports))
func collect_orphans(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int:
var orphans := 0
if not context._sub_context.is_empty():
orphans += add_orphan_report_test(context._sub_context[0], reports)
orphans += add_orphan_report_teststage(context, reports)
return orphans
func collect_reports(context :GdUnitExecutionContext) -> Array[GdUnitReport]:
var reports := context.reports()
var test_case := context.test_case
if test_case.is_interupted() and not test_case.is_expect_interupted() and test_case.report() != null:
reports.push_back(test_case.report())
# we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended`
if not context._sub_context.is_empty():
reports.append_array(context._sub_context[0].reports())
# needs finally to clean the test reports to avoid counting twice
context._sub_context[0].reports().clear()
return reports
func add_orphan_report_test(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int:
var orphans := context.count_orphans()
if orphans > 0:
reports.push_front(GdUnitReport.new()\
.create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans)))
return orphans
func add_orphan_report_teststage(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int:
var orphans := context.count_orphans()
if orphans > 0:
reports.push_front(GdUnitReport.new()\
.create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans)))
return orphans
func fire_test_skipped(context :GdUnitExecutionContext):
var test_suite := context.test_suite
var test_case := context.test_case
var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name
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_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, [report]))

View file

@ -0,0 +1,29 @@
## The test case startup hook implementation.[br]
## It executes the 'test_before()' block from the test-suite.
class_name GdUnitTestCaseBeforeStage
extends IGdUnitExecutionStage
var _test_name :StringName = ""
var _call_stage :bool
func _init(call_stage := true):
_call_stage = call_stage
func _execute(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name
fire_event(GdUnitEvent.new()\
.test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name))
if _call_stage:
@warning_ignore("redundant_await")
await test_suite.before_test()
context.error_monitor_start()
func set_test_name(test_name :StringName):
_test_name = test_name

View file

@ -0,0 +1,31 @@
## 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()
var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterizedExecutionStage.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
if test_case.is_parameterized():
await _stage_parameterized_test.execute(context)
elif test_case.is_fuzzed():
await _stage_fuzzer_test.execute(context)
else:
await _stage_single_test.execute(context)
func set_debug_mode(debug_mode :bool = false):
super.set_debug_mode(debug_mode)
_stage_single_test.set_debug_mode(debug_mode)
_stage_fuzzer_test.set_debug_mode(debug_mode)
_stage_parameterized_test.set_debug_mode(debug_mode)

View file

@ -0,0 +1,28 @@
## 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()
# unreference last used assert form the test to prevent memory leaks
GdUnitThreadManager.get_current_context().set_assert(null)
await context.gc()
var reports := context.reports()
var orphans := context.count_orphans()
if orphans > 0:
reports.push_front(GdUnitReport.new() \
.create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans)))
fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), context.build_report_statistics(orphans, false), reports))
GdUnitFileAccess.clear_tmp()
# Guard that checks if all doubled (spy/mock) objects are released
GdUnitClassDoubler.check_leaked_instances()

View file

@ -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(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count()))
@warning_ignore("redundant_await")
await test_suite.before()

View file

@ -0,0 +1,114 @@
## 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:
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.get_name())
await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case.get_name()))
# stop on first error or if fail fast is enabled
if _fail_fast and context.test_failed():
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().process_frame
context.test_suite.free()
context.dispose()
# 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().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)
GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter)
# finally free current test suite instance
test_suite.free()
await Engine.get_main_loop().process_frame
return _test_suite
func dispose_timers(test_suite :GdUnitTestSuite):
GdUnitTools.release_timers()
for child in test_suite.get_children():
if child is Timer:
child.stop()
test_suite.remove_child(child)
child.free()
func copy_properties(source :Object, target :Object):
if not source is _TestCase and not source is GdUnitTestSuite:
return
for property in source.get_property_list():
var property_name = 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(test_suite.get_script().resource_path, test_suite.get_name(), skip_count))
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(test_suite.get_script().resource_path, test_suite.get_name(), statistics, [report]))
await Engine.get_main_loop().process_frame
func set_debug_mode(debug_mode :bool = false):
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

View file

@ -0,0 +1,39 @@
## 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.
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:
context.set_active()
@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.
func _execute(_context :GdUnitExecutionContext) -> void:
@warning_ignore("assert_always_false")
assert(false, "The execution stage is not implemented")

View file

@ -0,0 +1,21 @@
## 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:
await _stage_before.execute(context)
if not context.test_case.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(context))
await _stage_after.execute(context)
func set_debug_mode(debug_mode :bool = false):
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)

View file

@ -0,0 +1,53 @@
## 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:
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()
# 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():
var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script(), fuzzer_arg.value_as_string())
fuzzer._iteration_index = 0
fuzzer._iteration_limit = test_case.iterations()
fuzzers.append(fuzzer)
return fuzzers

View file

@ -0,0 +1,22 @@
## The test case execution stage.[br]
class_name GdUnitTestCaseParameterizedExecutionStage
extends IGdUnitExecutionStage
var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false)
var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false)
var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseParamaterizedTestStage.new()
func _execute(context :GdUnitExecutionContext) -> void:
await _stage_before.execute(context)
if not context.test_case.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(context))
await _stage_after.execute(context)
func set_debug_mode(debug_mode :bool = false):
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)

View file

@ -0,0 +1,76 @@
## The parameterized test case execution stage.[br]
class_name GdUnitTestCaseParamaterizedTestStage
extends IGdUnitExecutionStage
var _stage_before: IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new()
var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new()
## Executes a parameterized test case.[br]
## It executes synchronized following stages[br]
## -> test_case( <test_parameters> ) [br]
func _execute(context: GdUnitExecutionContext) -> void:
var test_case := context.test_case
var test_parameter_index := test_case.test_parameter_index()
var is_fail := false
var is_error := false
var failing_index := 0
var parameter_set_resolver := test_case.parameter_set_resolver()
var test_names := parameter_set_resolver.build_test_case_names(test_case)
# if all parameter sets has static values we can preload and reuse it for better performance
var parameter_sets :Array = []
if parameter_set_resolver.is_parameter_sets_static():
parameter_sets = parameter_set_resolver.load_parameter_sets(test_case, true)
for parameter_set_index in test_names.size():
# is test_parameter_index is set, we run this parameterized test only
if test_parameter_index != -1 and test_parameter_index != parameter_set_index:
continue
var current_test_case_name = test_names[parameter_set_index]
_stage_before.set_test_name(current_test_case_name)
_stage_after.set_test_name(current_test_case_name)
var test_context := GdUnitExecutionContext.of(context)
await _stage_before.execute(test_context)
var current_parameter_set :Array
if parameter_set_resolver.is_parameter_set_static(parameter_set_index):
current_parameter_set = parameter_sets[parameter_set_index]
else:
current_parameter_set = _load_parameter_set(context, parameter_set_index)
if not test_case.is_interupted():
await test_case.execute_paramaterized(current_parameter_set)
await _stage_after.execute(test_context)
# we need to clean up the reports here so they are not reported twice
is_fail = is_fail or test_context.count_failures(false) > 0
is_error = is_error or test_context.count_errors(false) > 0
failing_index = parameter_set_index - 1
test_context.reports().clear()
if test_case.is_interupted():
break
# add report to parent execution context if failed or an error is found
if is_fail:
context.reports().append(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % failing_index))
if is_error:
context.reports().append(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % failing_index))
await context.gc()
func _load_parameter_set(context: GdUnitExecutionContext, parameter_set_index: int) -> Array:
var test_case := context.test_case
var test_suite := context.test_suite
# we need to exchange temporary for parameter resolving the execution context
# this is necessary because of possible usage of `auto_free` and needs to run in the parent execution context
var save_execution_context: GdUnitExecutionContext = test_suite.__execution_context
context.set_active()
var parameters := test_case.load_parameter_sets()
# restore the original execution context and restart the orphan monitor to get new instances into account
save_execution_context.set_active()
save_execution_context.orphan_monitor_start()
return parameters[parameter_set_index]
func set_debug_mode(debug_mode: bool=false):
super.set_debug_mode(debug_mode)
_stage_before.set_debug_mode(debug_mode)
_stage_after.set_debug_mode(debug_mode)

View file

@ -0,0 +1,22 @@
## 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:
await _stage_before.execute(context)
if not context.test_case.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(context))
await _stage_after.execute(context)
func set_debug_mode(debug_mode :bool = false):
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)

View file

@ -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()