Adding log.gd
This commit is contained in:
parent
eb32d6614e
commit
4522259397
547 changed files with 46844 additions and 0 deletions
192
addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd
Normal file
192
addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd
Normal 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()
|
||||
131
addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
Normal file
131
addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
Normal 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)
|
||||
|
|
@ -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)
|
||||
26
addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd
Normal file
26
addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd
Normal 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)
|
||||
|
|
@ -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]))
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue