summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/reporters
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/reporters
parent6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff)
escoria updateHEADmain
Diffstat (limited to 'addons/gdUnit4/src/reporters')
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitReportSummary.gd202
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitReportWriter.gd12
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd47
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitTestReporter.gd112
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd96
-rw-r--r--addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd235
-rw-r--r--addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd60
-rw-r--r--addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd199
-rw-r--r--addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd72
-rw-r--r--addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/html/template/.gdignore0
-rw-r--r--addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css66
-rw-r--r--addons/gdUnit4/src/reporters/html/template/css/logo.pngbin0 -> 49775 bytes
-rw-r--r--addons/gdUnit4/src/reporters/html/template/css/styles.css475
-rw-r--r--addons/gdUnit4/src/reporters/html/template/folder_report.html122
-rw-r--r--addons/gdUnit4/src/reporters/html/template/index.html164
-rw-r--r--addons/gdUnit4/src/reporters/html/template/suite_report.html177
-rw-r--r--addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd143
-rw-r--r--addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid1
-rw-r--r--addons/gdUnit4/src/reporters/xml/XmlElement.gd69
-rw-r--r--addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid1
29 files changed, 2262 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd
new file mode 100644
index 0000000..b55b964
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd
@@ -0,0 +1,202 @@
+class_name GdUnitReportSummary
+extends RefCounted
+
+var _resource_path: String
+var _name: String
+var _test_count := 0
+var _failure_count := 0
+var _error_count := 0
+var _orphan_count := 0
+var _skipped_count := 0
+var _flaky_count := 0
+var _duration := 0
+var _reports: Array[GdUnitReportSummary] = []
+var _text_formatter: Callable
+
+
+func _init(text_formatter: Callable) -> void:
+ _text_formatter = text_formatter
+
+
+func name() -> String:
+ return _name
+
+
+func path() -> String:
+ return _resource_path.get_base_dir().replace("res://", "")
+
+
+func get_resource_path() -> String:
+ return _resource_path
+
+
+func suite_count() -> int:
+ return _reports.size()
+
+
+func suite_executed_count() -> int:
+ var executed := _reports.size()
+ for report in _reports:
+ if report.test_count() == report.skipped_count():
+ executed -= 1
+ return executed
+
+
+func test_count() -> int:
+ var count := _test_count
+ for report in _reports:
+ count += report.test_count()
+ return count
+
+
+func test_executed_count() -> int:
+ return test_count() - skipped_count()
+
+
+func success_count() -> int:
+ return test_count() - error_count() - failure_count() - flaky_count() - skipped_count()
+
+
+func error_count() -> int:
+ return _error_count
+
+
+func failure_count() -> int:
+ return _failure_count
+
+
+func skipped_count() -> int:
+ return _skipped_count
+
+
+func flaky_count() -> int:
+ return _flaky_count
+
+
+func orphan_count() -> int:
+ return _orphan_count
+
+
+func duration() -> int:
+ return _duration
+
+
+func get_reports() -> Array:
+ return _reports
+
+
+func add_report(report: GdUnitReportSummary) -> void:
+ _reports.append(report)
+
+
+func report_state() -> String:
+ return calculate_state(error_count(), failure_count(), orphan_count(), flaky_count(), skipped_count())
+
+
+func succes_rate() -> String:
+ return calculate_succes_rate(test_count(), error_count(), failure_count())
+
+
+@warning_ignore("shadowed_variable")
+func add_testcase(resource_path: String, suite_name: String, test_name: String) -> void:
+ for report: GdUnitTestSuiteReport in _reports:
+ if report.get_resource_path() == resource_path:
+ var test_report := GdUnitTestCaseReport.new(resource_path, suite_name, test_name, _text_formatter)
+ report.add_or_create_test_report(test_report)
+
+
+func add_reports(
+ p_resource_path: String,
+ p_test_name: String,
+ p_reports: Array[GdUnitReport]) -> void:
+
+ for report:GdUnitTestSuiteReport in _reports:
+ if report.get_resource_path() == p_resource_path:
+ report.add_testcase_reports(p_test_name, p_reports)
+
+
+func add_testsuite_report(p_resource_path: String, p_suite_name: String, p_test_count: int) -> void:
+ _reports.append(GdUnitTestSuiteReport.new(p_resource_path, p_suite_name, p_test_count, _text_formatter))
+
+
+func add_testsuite_reports(
+ p_resource_path: String,
+ p_reports: Array = []) -> void:
+
+ for report:GdUnitTestSuiteReport in _reports:
+ if report.get_resource_path() == p_resource_path:
+ report.set_reports(p_reports)
+
+
+func set_counters(
+ p_resource_path: String,
+ p_test_name: String,
+ p_error_count: int,
+ p_failure_count: int,
+ p_orphan_count: int,
+ p_is_skipped: bool,
+ p_is_flaky: bool,
+ p_duration: int) -> void:
+
+ for report: GdUnitTestSuiteReport in _reports:
+ if report.get_resource_path() == p_resource_path:
+ report.set_testcase_counters(p_test_name, p_error_count, p_failure_count, p_orphan_count,
+ p_is_skipped, p_is_flaky, p_duration)
+
+
+func update_testsuite_counters(
+ p_resource_path: String,
+ p_error_count: int,
+ p_failure_count: int,
+ p_orphan_count: int,
+ p_skipped_count: int,
+ p_flaky_count: int,
+ p_duration: int) -> void:
+
+ for report:GdUnitTestSuiteReport in _reports:
+ if report.get_resource_path() == p_resource_path:
+ report._update_testsuite_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, p_duration)
+ _update_summary_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, 0)
+
+
+func _update_summary_counters(
+ p_error_count: int,
+ p_failure_count: int,
+ p_orphan_count: int,
+ p_skipped_count: int,
+ p_flaky_count: int,
+ p_duration: int) -> void:
+
+ _error_count += p_error_count
+ _failure_count += p_failure_count
+ _orphan_count += p_orphan_count
+ _skipped_count += p_skipped_count
+ _flaky_count += p_flaky_count
+ _duration += p_duration
+
+
+func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int, p_flaky_count: int, p_skipped_count: int) -> String:
+ if p_error_count > 0:
+ return "ERROR"
+ if p_failure_count > 0:
+ return "FAILED"
+ if p_flaky_count > 0:
+ return "FLAKY"
+ if p_orphan_count > 0:
+ return "WARNING"
+ if p_skipped_count > 0:
+ return "SKIPPED"
+ return "PASSED"
+
+
+func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String:
+ if p_failure_count == 0:
+ return "100%"
+ var count := p_test_count-p_failure_count-p_error_count
+ if count < 0:
+ return "0%"
+ return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%"
+
+
+func create_summary(_report_dir :String) -> String:
+ return ""
diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid
new file mode 100644
index 0000000..6092039
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid
@@ -0,0 +1 @@
+uid://koedaj41qehf
diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd
new file mode 100644
index 0000000..bf4c96e
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd
@@ -0,0 +1,12 @@
+class_name GdUnitReportWriter
+extends RefCounted
+
+
+func write(_report_path: String, _report: GdUnitReportSummary) -> String:
+ assert(false, "'write' is not implemented!")
+ return ""
+
+
+func output_format() -> String:
+ assert(false, "'output_format' is not implemented!")
+ return ""
diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid
new file mode 100644
index 0000000..c9be1dc
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid
@@ -0,0 +1 @@
+uid://dcm0384aabejh
diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd
new file mode 100644
index 0000000..2801696
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd
@@ -0,0 +1,47 @@
+class_name GdUnitTestCaseReport
+extends GdUnitReportSummary
+
+
+var _suite_name: String
+var _failure_reports: Array[GdUnitReport] = []
+
+
+func _init(p_resource_path: String, p_suite_name: String, p_test_name: String, text_formatter: Callable) -> void:
+ _resource_path = p_resource_path
+ _suite_name = p_suite_name
+ _name = p_test_name
+ _text_formatter = text_formatter
+
+
+func suite_name() -> String:
+ return _suite_name
+
+
+func failure_report() -> String:
+ var report_message := ""
+ for report in get_test_reports():
+ report_message += _text_formatter.call(str(report)) + "\n"
+ return report_message
+
+
+func add_testcase_reports(reports: Array[GdUnitReport]) -> void:
+ _failure_reports.append_array(reports)
+
+
+func set_testcase_counters(
+ p_error_count: int,
+ p_failure_count: int,
+ p_orphan_count: int,
+ p_is_skipped: bool,
+ p_is_flaky: bool,
+ p_duration: int) -> void:
+ _error_count = p_error_count
+ _failure_count = p_failure_count
+ _orphan_count = p_orphan_count
+ _skipped_count = p_is_skipped
+ _flaky_count = p_is_flaky as int
+ _duration = p_duration
+
+
+func get_test_reports() -> Array[GdUnitReport]:
+ return _failure_reports
diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid
new file mode 100644
index 0000000..eaf88d0
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid
@@ -0,0 +1 @@
+uid://dnts7oirmp8ad
diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd
new file mode 100644
index 0000000..46b8193
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd
@@ -0,0 +1,112 @@
+class_name GdUnitTestReporter
+extends RefCounted
+
+
+var _statistics := {}
+var _summary := {}
+
+
+func init_summary() -> void:
+ _summary["suite_count"] = 0
+ _summary["total_count"] = 0
+ _summary["error_count"] = 0
+ _summary["failed_count"] = 0
+ _summary["skipped_count"] = 0
+ _summary["flaky_count"] = 0
+ _summary["orphan_nodes"] = 0
+ _summary["elapsed_time"] = 0
+
+
+func init_statistics() -> void:
+ _statistics.clear()
+
+
+func add_test_statistics(event: GdUnitEvent) -> void:
+ _statistics[event.guid()] = {
+ "error_count" : event.error_count(),
+ "failed_count" : event.failed_count(),
+ "skipped_count" : event.skipped_count(),
+ "flaky_count" : event.is_flaky() as int,
+ "orphan_nodes" : event.orphan_nodes()
+ }
+
+
+func build_test_suite_statisitcs(event: GdUnitEvent) -> Dictionary:
+ var statistic := {
+ "total_count" : _statistics.size(),
+ "error_count" : event.error_count(),
+ "failed_count" : event.failed_count(),
+ "skipped_count" : event.skipped_count(),
+ "flaky_count" : 0,
+ "orphan_nodes" : event.orphan_nodes()
+ }
+ _summary["suite_count"] += 1
+ _summary["total_count"] += _statistics.size()
+ _summary["error_count"] += event.error_count()
+ _summary["failed_count"] += event.failed_count()
+ _summary["skipped_count"] += event.skipped_count()
+ _summary["orphan_nodes"] += event.orphan_nodes()
+ _summary["elapsed_time"] += event.elapsed_time()
+
+ for key: String in ["error_count", "failed_count", "skipped_count", "flaky_count", "orphan_nodes"]:
+ var value: int = _statistics.values().reduce(get_value.bind(key), 0 )
+ statistic[key] += value
+ _summary[key] += value
+
+ return statistic
+
+
+func get_value(acc: int, value: Dictionary, key: String) -> int:
+ return acc + value[key]
+
+
+func processed_suite_count() -> int:
+ return _summary["suite_count"]
+
+
+func total_test_count() -> int:
+ return _summary["total_count"]
+
+
+func total_flaky_count() -> int:
+ return _summary["flaky_count"]
+
+
+func total_error_count() -> int:
+ return _summary["error_count"]
+
+
+func total_failure_count() -> int:
+ return _summary["failed_count"]
+
+
+func total_skipped_count() -> int:
+ return _summary["skipped_count"]
+
+
+func total_orphan_count() -> int:
+ return _summary["orphan_nodes"]
+
+
+func elapsed_time() -> int:
+ return _summary["elapsed_time"]
+
+
+func error_count(statistics: Dictionary) -> int:
+ return statistics["error_count"]
+
+
+func failed_count(statistics: Dictionary) -> int:
+ return statistics["failed_count"]
+
+
+func orphan_nodes(statistics: Dictionary) -> int:
+ return statistics["orphan_nodes"]
+
+
+func skipped_count(statistics: Dictionary) -> int:
+ return statistics["skipped_count"]
+
+
+func flaky_count(statistics: Dictionary) -> int:
+ return statistics["flaky_count"]
diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid
new file mode 100644
index 0000000..30ffd28
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid
@@ -0,0 +1 @@
+uid://5u0wypoy6una
diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd
new file mode 100644
index 0000000..9be0682
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd
@@ -0,0 +1,96 @@
+class_name GdUnitTestSuiteReport
+extends GdUnitReportSummary
+
+var _time_stamp: int
+var _failure_reports: Array[GdUnitReport] = []
+
+
+func _init(p_resource_path: String, p_name: String, p_test_count: int, text_formatter: Callable) -> void:
+ _resource_path = p_resource_path
+ _name = p_name
+ _test_count = p_test_count
+ _time_stamp = Time.get_unix_time_from_system() as int
+ _text_formatter = text_formatter
+
+
+func failure_report() -> String:
+ var report_message := ""
+ for report in _failure_reports:
+ report_message += _text_formatter.call(str(report))
+ return report_message
+
+
+func set_duration(p_duration :int) -> void:
+ _duration = p_duration
+
+
+func time_stamp() -> int:
+ return _time_stamp
+
+
+func duration() -> int:
+ return _duration
+
+
+func set_skipped(skipped :int) -> void:
+ _skipped_count += skipped
+
+
+func set_orphans(orphans :int) -> void:
+ _orphan_count = orphans
+
+
+func set_failed(count :int) -> void:
+ _failure_count += count
+
+
+func set_reports(failure_reports :Array[GdUnitReport]) -> void:
+ _failure_reports = failure_reports
+
+
+func add_or_create_test_report(test_report: GdUnitTestCaseReport) -> void:
+ _reports.append(test_report)
+
+
+func _update_testsuite_counters(
+ p_error_count: int,
+ p_failure_count: int,
+ p_orphan_count: int,
+ p_skipped_count: int,
+ p_flaky_count: int,
+ p_duration: int) -> void:
+ _error_count += p_error_count
+ _failure_count += p_failure_count
+ _orphan_count += p_orphan_count
+ _skipped_count += p_skipped_count
+ _flaky_count += p_flaky_count
+ _duration += p_duration
+
+
+func set_testcase_counters(
+ test_name: String,
+ p_error_count: int,
+ p_failure_count: int,
+ p_orphan_count: int,
+ p_is_skipped: bool,
+ p_is_flaky: bool,
+ p_duration: int) -> void:
+ if _reports.is_empty():
+ return
+ var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool:
+ return report.name() == test_name
+ ).back()
+ if test_report:
+ test_report.set_testcase_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration)
+
+
+func add_testcase_reports(test_name: String, reports: Array[GdUnitReport]) -> void:
+ if reports.is_empty():
+ return
+ # we lookup to latest matching report because of flaky tests could be retry the tests
+ # and resultis in multipe report entries with the same name
+ var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool:
+ return report.name() == test_name
+ ).back()
+ if test_report:
+ test_report.add_testcase_reports(reports)
diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid
new file mode 100644
index 0000000..2784de4
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid
@@ -0,0 +1 @@
+uid://dyii7lcvudbf8
diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd
new file mode 100644
index 0000000..9978deb
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd
@@ -0,0 +1,235 @@
+@tool
+class_name GdUnitConsoleTestReporter
+
+
+var test_session: GdUnitTestSession:
+ get:
+ return test_session
+ set(value):
+ # disconnect first possible connected listener
+ if test_session != null:
+ test_session.test_event.disconnect(on_gdunit_event)
+ # add listening to current session
+ test_session = value
+ if test_session != null:
+ test_session.test_event.connect(on_gdunit_event)
+
+
+var _writer: GdUnitMessageWriter
+var _reporter: GdUnitTestReporter = GdUnitTestReporter.new()
+var _status_indent := 86
+var _detailed: bool
+var _text_color: Color = Color.ANTIQUE_WHITE
+var _function_color: Color = Color.ANTIQUE_WHITE
+var _engine_type_color: Color = Color.ANTIQUE_WHITE
+
+
+func _init(writer: GdUnitMessageWriter, detailed := false) -> void:
+ _writer = writer
+ _writer.clear()
+ _detailed = detailed
+ if _detailed:
+ _status_indent = 20
+ init_colors()
+
+
+func init_colors() -> void:
+ if Engine.is_editor_hint():
+ var settings := EditorInterface.get_editor_settings()
+ _text_color = settings.get_setting("text_editor/theme/highlighting/text_color")
+ _function_color = settings.get_setting("text_editor/theme/highlighting/function_color")
+ _engine_type_color = settings.get_setting("text_editor/theme/highlighting/engine_type_color")
+
+
+func clear() -> void:
+ _writer.clear()
+
+
+func on_gdunit_event(event: GdUnitEvent) -> void:
+ match event.type():
+ GdUnitEvent.INIT:
+ _reporter.init_summary()
+
+ GdUnitEvent.STOP:
+ _print_summary()
+ println_message(build_executed_test_suite_msg(processed_suite_count(), processed_suite_count()), Color.DARK_SALMON)
+ println_message(build_executed_test_case_msg(total_test_count(), total_skipped_count()), Color.DARK_SALMON)
+ println_message("Total execution time: %s" % LocalTime.elapsed(elapsed_time()), Color.DARK_SALMON)
+ # We need finally to set the wave effect to enable the animations
+ _writer.effect(GdUnitMessageWriter.Effect.WAVE).print_at("", 0)
+
+ GdUnitEvent.TESTSUITE_BEFORE:
+ _reporter.init_statistics()
+ print_message("Run Test Suite: ", Color.DARK_TURQUOISE)
+ println_message(event.resource_path(), _engine_type_color)
+
+ GdUnitEvent.TESTSUITE_AFTER:
+ if not event.reports().is_empty():
+ _writer.indent(1).color(_engine_type_color).print_message(event._suite_name)
+ print_message(" > ")
+ print_message("finalize()", _function_color)
+ _print_failure_report(event.reports())
+ _print_statistics(_reporter.build_test_suite_statisitcs(event))
+ _print_status(event)
+ println_message("")
+ if _detailed:
+ println_message("")
+
+ GdUnitEvent.TESTCASE_BEFORE:
+ var test := test_session.find_test_by_id(event.guid())
+ _print_test_path(test, event.guid())
+ if _detailed:
+ _writer.color(Color.FOREST_GREEN).print_at("STARTED", _status_indent)
+ println_message("")
+
+ GdUnitEvent.TESTCASE_AFTER:
+ _reporter.add_test_statistics(event)
+ if _detailed:
+ var test := test_session.find_test_by_id(event.guid())
+ _print_test_path(test, event.guid())
+ _print_status(event)
+ _print_failure_report(event.reports())
+ if _detailed:
+ println_message("")
+
+
+func _print_test_path(test: GdUnitTestCase, uid: GdUnitGUID) -> void:
+ if test == null:
+ prints_warning("Can't print full test info, the test by uid: '%s' was not discovered." % uid)
+ _writer.indent(1).color(_engine_type_color).print_message("Test ID: %s" % uid)
+ return
+
+ var suite_name := test.source_file if _detailed else test.suite_name
+ _writer.indent(1).color(_engine_type_color).print_message(suite_name)
+ print_message(" > ")
+ print_message(test.display_name, _function_color)
+
+
+func _print_status(event: GdUnitEvent) -> void:
+ if event.is_flaky() and event.is_success():
+ var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT)
+ _writer.color(Color.GREEN_YELLOW) \
+ .style(GdUnitMessageWriter.ITALIC) \
+ .print_at("FLAKY (%d retries)" % retries, _status_indent)
+ elif event.is_success():
+ _writer.color(Color.FOREST_GREEN).print_at("PASSED", _status_indent)
+ elif event.is_skipped():
+ _writer.color(Color.GOLDENROD).style(GdUnitMessageWriter.ITALIC).print_at("SKIPPED", _status_indent)
+ elif event.is_failed() or event.is_error():
+ var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT)
+ var message := "FAILED (retry %d)" % retries if retries > 1 else "FAILED"
+ _writer.color(Color.FIREBRICK) \
+ .style(GdUnitMessageWriter.BOLD) \
+ .effect(GdUnitMessageWriter.Effect.WAVE) \
+ .print_at(message, _status_indent)
+ elif event.is_warning():
+ _writer.color(Color.GOLDENROD) \
+ .style(GdUnitMessageWriter.UNDERLINE) \
+ .print_at("WARNING", _status_indent)
+
+ println_message(" %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE)
+
+
+func _print_failure_report(reports: Array[GdUnitReport]) -> void:
+ for report in reports:
+ if (
+ report.is_failure()
+ or report.is_error()
+ or report.is_warning()
+ or report.is_skipped()
+ or report.is_orphan()
+ ):
+ _writer.indent(1) \
+ .color(Color.DARK_TURQUOISE) \
+ .style(GdUnitMessageWriter.BOLD | GdUnitMessageWriter.UNDERLINE) \
+ .println_message("Report:")
+ var text := str(report)
+ for line in text.split("\n", false):
+ _writer.indent(2).color(Color.DARK_TURQUOISE).println_message(line)
+
+ if not reports.is_empty():
+ println_message("")
+
+
+func _print_statistics(statistics: Dictionary) -> void:
+ print_message("Statistics:", Color.DODGER_BLUE)
+ print_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % \
+ [statistics["total_count"],
+ statistics["error_count"],
+ statistics["failed_count"],
+ statistics["flaky_count"],
+ statistics["skipped_count"],
+ statistics["orphan_nodes"]])
+
+
+func _print_summary() -> void:
+ print_message("Overall Summary:", Color.DODGER_BLUE)
+ _writer \
+ .println_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % [
+ total_test_count(),
+ total_error_count(),
+ total_failure_count(),
+ total_flaky_count(),
+ total_skipped_count(),
+ total_orphan_count()
+ ])
+
+
+func build_executed_test_suite_msg(executed_count: int, total_count: int) -> String:
+ if executed_count == total_count:
+ return "Executed test suites: (%d/%d)" % [executed_count, total_count]
+ return "Executed test suites: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)]
+
+
+func build_executed_test_case_msg(total_count: int, p_skipped_count: int) -> String:
+ if p_skipped_count == 0:
+ return "Executed test cases : (%d/%d)" % [total_count, total_count]
+ return "Executed test cases : (%d/%d), %d skipped" % [total_count - p_skipped_count, total_count, p_skipped_count]
+
+
+func print_message(message: String, color: Color = _text_color) -> void:
+ _writer.color(color).print_message(message)
+
+
+func println_message(message: String, color: Color = _text_color) -> void:
+ _writer.color(color).println_message(message)
+
+
+func prints_warning(message: String) -> void:
+ _writer.prints_warning(message)
+
+
+func prints_error(message: String) -> void:
+ _writer.prints_error(message)
+
+
+func total_test_count() -> int:
+ return _reporter.total_test_count()
+
+
+func total_error_count() -> int:
+ return _reporter.total_error_count()
+
+
+func total_failure_count() -> int:
+ return _reporter.total_failure_count()
+
+
+func total_flaky_count() -> int:
+ return _reporter.total_flaky_count()
+
+
+func total_skipped_count() -> int:
+ return _reporter.total_skipped_count()
+
+
+func total_orphan_count() -> int:
+ return _reporter.total_orphan_count()
+
+
+func processed_suite_count() -> int:
+ return _reporter.processed_suite_count()
+
+
+func elapsed_time() -> int:
+ return _reporter.elapsed_time()
diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid
new file mode 100644
index 0000000..7ca4b35
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid
@@ -0,0 +1 @@
+uid://d0bs18q20xbdb
diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd
new file mode 100644
index 0000000..74b961b
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd
@@ -0,0 +1,60 @@
+class_name GdUnitByPathReport
+extends GdUnitReportSummary
+
+
+func _init(p_path :String, report_summaries :Array[GdUnitReportSummary]) -> void:
+ _resource_path = p_path
+ _reports = report_summaries
+
+
+# -> Dictionary[String, Array[GdUnitReportSummary]]
+static func sort_reports_by_path(report_summaries :Array[GdUnitReportSummary]) -> Dictionary:
+ var by_path := Dictionary()
+ for report in report_summaries:
+ var suite_path :String = ProjectSettings.localize_path(report.path())
+ var suite_report :Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary])
+ suite_report.append(report)
+ by_path[suite_path] = suite_report
+ return by_path
+
+
+func path() -> String:
+ return _resource_path.replace("res://", "").trim_suffix("/")
+
+
+func create_record(report_link :String) -> String:
+ return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_PATH, self, report_link)
+
+
+func write(report_dir :String) -> String:
+ calculate_summary()
+ var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/folder_report.html")
+ var path_report := GdUnitHtmlPatterns.build(template, self, "")
+ path_report = apply_testsuite_reports(report_dir, path_report, _reports)
+
+ var output_path := "%s/path/%s.html" % [report_dir, path().replace("/", ".")]
+ var dir := output_path.get_base_dir()
+ if not DirAccess.dir_exists_absolute(dir):
+ @warning_ignore("return_value_discarded")
+ DirAccess.make_dir_recursive_absolute(dir)
+ FileAccess.open(output_path, FileAccess.WRITE).store_string(path_report)
+ return output_path
+
+
+func apply_testsuite_reports(report_dir :String, template :String, test_suite_reports :Array[GdUnitReportSummary]) -> String:
+ var table_records := PackedStringArray()
+ for report:GdUnitTestSuiteReport in test_suite_reports:
+ var report_link := GdUnitHtmlReportWriter.create_output_path(report_dir, report.path(), report.name()).replace(report_dir, "..")
+ @warning_ignore("return_value_discarded")
+ table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report))
+ return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records))
+
+
+func calculate_summary() -> void:
+ for report:GdUnitTestSuiteReport in get_reports():
+ _error_count += report.error_count()
+ _failure_count += report.failure_count()
+ _orphan_count += report.orphan_count()
+ _skipped_count += report.skipped_count()
+ _flaky_count += report.flaky_count()
+ _duration += report.duration()
diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid
new file mode 100644
index 0000000..607e423
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid
@@ -0,0 +1 @@
+uid://ct0fnu7t8q2lw
diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd
new file mode 100644
index 0000000..b8be904
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd
@@ -0,0 +1,199 @@
+class_name GdUnitHtmlPatterns
+extends RefCounted
+
+const TABLE_RECORD_TESTSUITE = """
+ <tr class="${report_state}">
+ <td><a href=${report_link}>${testsuite_name}</a></td>
+ <td><span class="status status-${report_state}">${report_state_label}</span></td>
+ <td>${test_count}</td>
+ <td>${skipped_count}</td>
+ <td>${flaky_count}</td>
+ <td>${failure_count}</td>
+ <td>${orphan_count}</td>
+ <td>${duration}</td>
+ <td>
+ <div class="status-bar">
+ <div class="status-bar-column status-skipped" style="width: ${skipped-percent};"></div>
+ <div class="status-bar-column status-passed" style="width: ${passed-percent};"></div>
+ <div class="status-bar-column status-flaky" style="width: ${flaky-percent};"></div>
+ <div class="status-bar-column status-error" style="width: ${error-percent};"></div>
+ <div class="status-bar-column status-failed" style="width: ${failed-percent};"></div>
+ <div class="status-bar-column status-warning" style="width: ${warning-percent};"></div>
+ </div>
+ </td>
+ </tr>
+"""
+
+const TABLE_RECORD_PATH = """
+ <tr class="${report_state}">
+ <td><a class="${report_state}" href="${report_link}">${path}</a></td>
+ <td><span class="status status-${report_state}">${report_state_label}</span></td>
+ <td>${test_count}</td>
+ <td>${skipped_count}</td>
+ <td>${flaky_count}</td>
+ <td>${failure_count}</td>
+ <td>${orphan_count}</td>
+ <td>${duration}</td>
+ <td>
+ <div class="status-bar">
+ <div class="status-bar-column status-skipped" style="width: ${skipped-percent};"></div>
+ <div class="status-bar-column status-passed" style="width: ${passed-percent};"></div>
+ <div class="status-bar-column status-flaky" style="width: ${flaky-percent};"></div>
+ <div class="status-bar-column status-error" style="width: ${error-percent};"></div>
+ <div class="status-bar-column status-failed" style="width: ${failed-percent};"></div>
+ <div class="status-bar-column status-warning" style="width: ${warning-percent};"></div>
+ </div>
+ </td>
+ </tr>
+"""
+
+
+const TABLE_REPORT_TESTSUITE = """
+ <tr class="${report_state}">
+ <td>TestSuite hooks</td>
+ <td>n/a</td>
+ <td>${orphan_count}</td>
+ <td>${duration}</td>
+ <td class="report-column">
+ <pre>
+${failure-report}
+ </pre>
+ </td>
+ </tr>
+"""
+
+
+const TABLE_RECORD_TESTCASE = """
+ <tr class="testcase-group">
+ <td>${testcase_name}</td>
+ <td><span class="status status-${report_state}">${report_state_label}</span></td>
+ <td>${skipped_count}</td>
+ <td>${orphan_count}</td>
+ <td>${duration}</td>
+ <td class="report-column">
+ <pre>
+${failure-report}
+ </pre>
+ </td>
+ </tr>
+"""
+
+const CHARACTERS_TO_ENCODE := {
+ '<' : '&lt;',
+ '>' : '&gt;'
+}
+
+const TABLE_BY_PATHS = "${report_table_paths}"
+const TABLE_BY_TESTSUITES = "${report_table_testsuites}"
+const TABLE_BY_TESTCASES = "${report_table_tests}"
+
+# the report state success, error, warning
+const REPORT_STATE = "${report_state}"
+const REPORT_STATE_LABEL = "${report_state_label}"
+const PATH = "${path}"
+const RESOURCE_PATH = "${resource_path}"
+const TESTSUITE_COUNT = "${suite_count}"
+const TESTCASE_COUNT = "${test_count}"
+const FAILURE_COUNT = "${failure_count}"
+const FLAKY_COUNT = "${flaky_count}"
+const SKIPPED_COUNT = "${skipped_count}"
+const ORPHAN_COUNT = "${orphan_count}"
+const DURATION = "${duration}"
+const FAILURE_REPORT = "${failure-report}"
+const SUCCESS_PERCENT = "${success_percent}"
+
+
+const QUICK_STATE_SKIPPED = "${skipped-percent}"
+const QUICK_STATE_PASSED = "${passed-percent}"
+const QUICK_STATE_FLAKY = "${flaky-percent}"
+const QUICK_STATE_ERROR = "${error-percent}"
+const QUICK_STATE_FAILED = "${failed-percent}"
+const QUICK_STATE_WARNING = "${warning-percent}"
+
+const TESTSUITE_NAME = "${testsuite_name}"
+const TESTCASE_NAME = "${testcase_name}"
+const REPORT_LINK = "${report_link}"
+const BREADCRUMP_PATH_LINK = "${breadcrumb_path_link}"
+const BUILD_DATE = "${buid_date}"
+
+
+static func current_date() -> String:
+ return Time.get_datetime_string_from_system(true, true)
+
+
+static func build(template: String, report: GdUnitReportSummary, report_link: String) -> String:
+ return template\
+ .replace(PATH, get_report_path(report))\
+ .replace(BREADCRUMP_PATH_LINK, get_path_as_link(report))\
+ .replace(RESOURCE_PATH, report.get_resource_path())\
+ .replace(TESTSUITE_NAME, html_encoded(report.name()))\
+ .replace(TESTSUITE_COUNT, str(report.suite_count()))\
+ .replace(TESTCASE_COUNT, str(report.test_count()))\
+ .replace(FAILURE_COUNT, str(report.error_count() + report.failure_count()))\
+ .replace(FLAKY_COUNT, str(report.flaky_count()))\
+ .replace(SKIPPED_COUNT, str(report.skipped_count()))\
+ .replace(ORPHAN_COUNT, str(report.orphan_count()))\
+ .replace(DURATION, LocalTime.elapsed(report.duration()))\
+ .replace(SUCCESS_PERCENT, report.calculate_succes_rate(report.test_count(), report.error_count(), report.failure_count()))\
+ .replace(REPORT_STATE, report.report_state().to_lower())\
+ .replace(REPORT_STATE_LABEL, report.report_state())\
+ .replace(QUICK_STATE_SKIPPED, calculate_percentage(report.test_count(), report.skipped_count()))\
+ .replace(QUICK_STATE_PASSED, calculate_percentage(report.test_count(), report.success_count()))\
+ .replace(QUICK_STATE_FLAKY, calculate_percentage(report.test_count(), report.flaky_count()))\
+ .replace(QUICK_STATE_ERROR, calculate_percentage(report.test_count(), report.error_count()))\
+ .replace(QUICK_STATE_FAILED, calculate_percentage(report.test_count(), report.failure_count()))\
+ .replace(QUICK_STATE_WARNING, calculate_percentage(report.test_count(), 0))\
+ .replace(REPORT_LINK, report_link)\
+ .replace(BUILD_DATE, current_date())
+
+
+static func load_template(template_name :String) -> String:
+ return FileAccess.open(template_name, FileAccess.READ).get_as_text()
+
+
+static func get_path_as_link(report: GdUnitReportSummary) -> String:
+ return "../path/%s.html" % report.path().replace("/", ".")
+
+
+static func get_report_path(report: GdUnitReportSummary) -> String:
+ var path := report.path()
+ if path.is_empty():
+ return "/"
+ return path
+
+
+static func calculate_percentage(p_test_count: int, count: int) -> String:
+ if count <= 0:
+ return "0%"
+ return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%"
+
+
+static func html_encoded(value: String) -> String:
+ for key: String in CHARACTERS_TO_ENCODE.keys():
+ @warning_ignore("unsafe_cast")
+ value = value.replace(key, CHARACTERS_TO_ENCODE[key] as String)
+ return value
+
+
+static func create_suite_record(report_link: String, report: GdUnitTestSuiteReport) -> String:
+ return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_TESTSUITE, report, report_link)
+
+
+static func create_test_failure_report(_report_dir :String, report: GdUnitTestCaseReport) -> String:
+ return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\
+ .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\
+ .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\
+ .replace(GdUnitHtmlPatterns.TESTCASE_NAME, report.name())\
+ .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(report.skipped_count()))\
+ .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\
+ .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\
+ .replace(GdUnitHtmlPatterns.FAILURE_REPORT, report.failure_report())
+
+
+static func create_suite_failure_report(report: GdUnitTestSuiteReport) -> String:
+ return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\
+ .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\
+ .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\
+ .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\
+ .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\
+ .replace(GdUnitHtmlPatterns.FAILURE_REPORT, report.failure_report())
diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid
new file mode 100644
index 0000000..aa548b9
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid
@@ -0,0 +1 @@
+uid://g7tdvdatyesm
diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd
new file mode 100644
index 0000000..a3d2bff
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd
@@ -0,0 +1,72 @@
+class_name GdUnitHtmlReportWriter
+extends GdUnitReportWriter
+
+
+func output_format() -> String:
+ return "HTML"
+
+
+func write(report_path: String, report: GdUnitReportSummary) -> String:
+ var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/index.html")
+ var to_write := GdUnitHtmlPatterns.build(template, report, "")
+ to_write = _apply_path_reports(report_path, to_write, report.get_reports())
+ to_write = _apply_testsuite_reports(report_path, to_write, report.get_reports())
+ # write report
+ DirAccess.make_dir_recursive_absolute(report_path)
+ var html_report_file := "%s/index.html" % report_path
+ FileAccess.open(html_report_file, FileAccess.WRITE).store_string(to_write)
+ @warning_ignore("return_value_discarded")
+ GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/reporters/html/template/css/", report_path + "/css")
+ return html_report_file
+
+
+func _apply_path_reports(report_dir: String, template: String, report_summaries: Array) -> String:
+ #Dictionary[String, Array[GdUnitReportSummary]]
+ var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(report_summaries)
+ var table_records := PackedStringArray()
+ var paths: Array[String] = []
+ paths.append_array(path_report_mapping.keys())
+ paths.sort()
+ for report_at_path in paths:
+ var reports: Array[GdUnitReportSummary] = path_report_mapping.get(report_at_path)
+ var report := GdUnitByPathReport.new(report_at_path, reports)
+ var report_link: String = report.write(report_dir).replace(report_dir, ".")
+ @warning_ignore("return_value_discarded")
+ table_records.append(report.create_record(report_link))
+ return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records))
+
+
+func _apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String:
+ var table_records := PackedStringArray()
+ for report: GdUnitTestSuiteReport in test_suite_reports:
+ var report_link: String = _write(report_dir, report).replace(report_dir, ".")
+ @warning_ignore("return_value_discarded")
+ table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report))
+ return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records))
+
+
+func _write(report_dir :String, report: GdUnitTestSuiteReport) -> String:
+ var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/suite_report.html")
+ template = GdUnitHtmlPatterns.build(template, report, "")
+
+ var report_output_path := create_output_path(report_dir, report.path(), report.name())
+ var test_report_table := PackedStringArray()
+ if not report._failure_reports.is_empty():
+ @warning_ignore("return_value_discarded")
+ test_report_table.append(GdUnitHtmlPatterns.create_suite_failure_report(report))
+ for test_report: GdUnitTestCaseReport in report._reports:
+ @warning_ignore("return_value_discarded")
+ test_report_table.append(GdUnitHtmlPatterns.create_test_failure_report(report_output_path, test_report))
+
+ template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table))
+
+ var dir := report_output_path.get_base_dir()
+ if not DirAccess.dir_exists_absolute(dir):
+ @warning_ignore("return_value_discarded")
+ DirAccess.make_dir_recursive_absolute(dir)
+ FileAccess.open(report_output_path, FileAccess.WRITE).store_string(template)
+ return report_output_path
+
+
+static func create_output_path(report_dir :String, path: String, name: String) -> String:
+ return "%s/test_suites/%s.%s.html" % [report_dir, path.replace("/", "."), name]
diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid
new file mode 100644
index 0000000..8a9edd6
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid
@@ -0,0 +1 @@
+uid://co0bj8mioxbsk
diff --git a/addons/gdUnit4/src/reporters/html/template/.gdignore b/addons/gdUnit4/src/reporters/html/template/.gdignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/.gdignore
diff --git a/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css
new file mode 100644
index 0000000..17215ff
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css
@@ -0,0 +1,66 @@
+.breadcrumb {
+ display: flex;
+ border-radius: 6px;
+ overflow: hidden;
+ height: 45px;
+ z-index: 1;
+ background-color: #9d73eb;
+ margin-top: 0px;
+ margin-bottom: 10px;
+ box-shadow: 0 0 3px black;
+}
+
+.breadcrumb a {
+ position: relative;
+ display: flex;
+ -ms-flex-positive: 1;
+ flex-grow: 1;
+ text-decoration: none;
+ margin: auto;
+ height: 100%;
+ color: white;
+}
+
+.breadcrumb a:first-child {
+ padding-left: 5.2px;
+}
+
+.breadcrumb a:last-child {
+ padding-right: 5.2px;
+}
+
+.breadcrumb a:after {
+ content: "";
+ position: absolute;
+ display: inline-block;
+ width: 45px;
+ height: 45px;
+ top: 0;
+ right: -20px;
+ background-color: #9d73eb;
+ border-top-right-radius: 5px;
+ transform: scale(0.707) rotate(45deg);
+ box-shadow: 2px -2px rgba(0, 0, 0, 0.25);
+ z-index: 1;
+}
+
+.breadcrumb a:last-child:after {
+ content: none;
+}
+
+.breadcrumb a.active,
+.breadcrumb a:hover {
+ background: #b899f2;
+ color: white;
+ text-decoration: underline;
+}
+
+.breadcrumb a.active:after,
+.breadcrumb a:hover:after {
+ background: #b899f2;
+}
+
+.breadcrumb span {
+ margin: inherit;
+ z-index: 2;
+}
diff --git a/addons/gdUnit4/src/reporters/html/template/css/logo.png b/addons/gdUnit4/src/reporters/html/template/css/logo.png
new file mode 100644
index 0000000..12de79f
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/css/logo.png
Binary files differ
diff --git a/addons/gdUnit4/src/reporters/html/template/css/styles.css b/addons/gdUnit4/src/reporters/html/template/css/styles.css
new file mode 100644
index 0000000..e92d59b
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/css/styles.css
@@ -0,0 +1,475 @@
+html,
+body {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ font-family: sans-serif;
+ background-color: white;
+ height: 100%;
+}
+
+main {
+ flex-grow: 1;
+ overflow: auto;
+ margin: 0 10em;
+}
+
+
+header {
+ color: white;
+ padding: 1px;
+ position: relative;
+ background-image: linear-gradient(to bottom right, #8058e3, #9d73eb);
+}
+
+.logo {
+ position: fixed;
+ top: 20px;
+ left: 20px;
+ display: flex;
+ align-items: center;
+ z-index: 1000;
+ filter: grayscale(1);
+ mix-blend-mode: plus-lighter;
+}
+
+.logo img {
+ width: 64px;
+ height: 64px;
+}
+
+.logo span {
+ font-size: 1.2em;
+ color: lightslategray;
+}
+
+.report-container {
+ margin: 0 15em;
+ text-align: center;
+ margin-top: 60px;
+ flex-grow: 0;
+}
+
+h1 {
+ margin: 0 0 20px 0;
+ font-size: 2.5em;
+ font-weight: normal;
+}
+
+.summary {
+ display: inline-flex;
+ justify-content: center;
+ flex-wrap: nowrap;
+ margin-bottom: 20px;
+ align-items: baseline;
+ max-width: 960px;
+}
+
+.summary-item {
+ flex: 1;
+ min-width: 80px;
+}
+
+.label {
+ font-size: 1em;
+ flex-wrap: nowrap;
+}
+
+.value {
+ font-size: 0.9em;
+ display: block;
+ padding-top: 10px;
+ color: lightgray;
+}
+
+.success-rate {
+ padding-left: 40px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.check-icon {
+ background-color: #34c538;
+ color: white;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.4em;
+}
+
+.rate-text {
+ text-align: center;
+ flex-wrap: nowrap;
+}
+
+.percentage {
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+
+nav {
+ padding: 20px 0px;
+ font-family: monospace;
+}
+
+nav ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ justify-content: flex-start;
+ border-bottom: 1px solid lightgray;
+}
+
+nav li {
+ cursor: pointer;
+ padding: 5px 20px;
+ font-size: 1.1em;
+ color: lightslategray;
+}
+
+nav li.active {
+ color: darkslategray;
+ border-bottom: 1px solid darkslategray;
+ font-weight: bold;
+}
+
+div#content {
+ height: calc(100vh - 400px);
+}
+
+
+table {
+ width: 100%;
+ height: 100%;
+ border-collapse: collapse;
+ overflow: hidden;
+}
+
+thead th {
+ position: sticky;
+ top: 0;
+ background-color: white;
+ z-index: 1;
+ border-bottom: 2px solid #ddd;
+}
+
+tbody {
+ display: block;
+ /* Limit the height of the table body */
+ max-height: calc(100vh - 400px);
+ /* Enable scrolling on the table body */
+ overflow-y: auto;
+}
+
+thead,
+tbody tr {
+ display: table;
+ width: 100%;
+ table-layout: fixed;
+}
+
+tbody td {
+ overflow: hidden;
+}
+
+/* Ensure scrollbar visibility */
+tbody::-webkit-scrollbar {
+ height: 4px;
+ width: 14px;
+}
+
+tbody::-webkit-scrollbar-thumb {
+ background-color: #aaa6a6;
+ border-radius: 4px;
+}
+
+tbody::-webkit-scrollbar-track {
+ background-color: #f1f1f1;
+}
+
+th,
+td {
+ font-size: .9em;
+ padding: 5px 0px;
+ border-bottom: 1px solid #eee;
+ color: lightslategrey;
+ text-align: left;
+ text-wrap: nowrap;
+ /* Default max and min width for all columns */
+ max-width: 150px;
+ min-width: 80px;
+ width: 80px;
+}
+
+th {
+ font-size: 1em;
+ font-weight: normal;
+ padding-top: 20px;
+ color: gray;
+ text-wrap: nowrap;
+}
+
+.tab-report {
+ display: grid;
+ grid-template-columns: 100%;
+ margin-bottom: 20px;
+}
+
+.tab-report-grid {
+ display: grid;
+ grid-template-columns: 70% 30%;
+ margin-bottom: 20px;
+}
+
+
+/* Specific styling for the first column (Testcase) */
+th:first-child,
+td:first-child {
+ padding-left: 5px;
+ text-align: left;
+ /* Max width for the first column */
+ min-width: 249px;
+ width: 250px;
+ /* Enable scrollbar if content exceeds max-width */
+ white-space: nowrap;
+ overflow: auto;
+}
+
+/* Scrollbar styles for first column */
+td:first-child {
+ overflow-x: auto;
+ text-overflow: initial;
+}
+
+/* Scrollbar appearance */
+td:first-child::-webkit-scrollbar {
+ height: 6px;
+}
+
+td:first-child::-webkit-scrollbar-thumb {
+ background-color: #888;
+ border-radius: 10px;
+}
+
+td:first-child::-webkit-scrollbar-track {
+ background-color: #f1f1f1;
+}
+
+/* Max width for Result column */
+th:nth-child(2),
+td:nth-child(2) {
+ max-width: 140px;
+ min-width: 140px;
+ width: 140px;
+}
+
+/* Max width for Quick Results column */
+th:nth-child(9),
+td:nth-child(9) {
+ max-width: 140px;
+ min-width: 140px;
+ width: 140px;
+ padding-right: 10px;
+}
+
+/* Background color for alternating groups */
+.group-bg-1 {
+ background-color: #f1f1f1;
+}
+
+.group-bg-2 {
+ background-color: #e0e0e0;
+}
+
+.grid-item {
+ overflow: auto;
+ padding-left: 20px;
+ color: lightslategrey;
+ max-height: calc(100vh - 350px);
+}
+
+div.tab td.report-column,
+th.report-column {
+ display: none;
+}
+
+/* Result status styles */
+.status {
+ padding: 2px 40px;
+ border-radius: 6px;
+ color: black;
+ width: 40px;
+ display: flex;
+ align-content: center;
+ align-items: center;
+}
+
+.status-bar {
+ display: flex;
+ border-radius: 8px;
+ overflow: hidden;
+ height: 20px;
+ flex-wrap: nowrap;
+ justify-content: space-evenly;
+}
+
+.status-bar-column {
+ margin: -2px;
+ color: black;
+ display: flex;
+ align-content: center;
+ align-items: center;
+ transition: width 0.3s ease;
+}
+
+.status-skipped {
+ background-color: #888888;
+}
+
+.status-passed {
+ background-color: #63bb38;
+}
+
+.status-error {
+ background-color: #fd1100;
+}
+
+.status-failed {
+ background-color: #ed594f;
+}
+
+.status-flaky {
+ background-color: #1d9a1f;
+}
+
+.status-warning {
+ background-color: #fdda3f;
+}
+
+div.tab tr:hover {
+ background-color: #d9e7fa;
+ box-shadow: 0 0 5px black;
+}
+
+div.tab tr.selected {
+ background-color: #d9e7fa;
+}
+
+div.report-column {
+ margin-top: 10px;
+ width: 100%;
+ text-align: left;
+}
+
+.logging-container {
+ width: 100%;
+ height: 100%;
+}
+
+div.godot-report-frame {
+ margin: 10px;
+ font-family: monospace;
+ height: 100%;
+ background-color: #eee;
+}
+
+div.include-footer {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ display: flex;
+}
+
+footer {
+ position: static;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ white-space: nowrap;
+ color: lightgray;
+ font-size: 12px;
+ background-image: linear-gradient(to bottom right, #8058e3, #9d73eb);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+footer p {
+ padding-left: 10em;
+}
+
+footer .status-legend {
+ display: flex;
+ gap: 15px;
+ width: 500px;
+}
+
+footer a {
+ color: lightgray;
+}
+
+footer a:hover {
+ color: whitesmoke;
+}
+
+footer a:visited {
+ color: whitesmoke;
+}
+
+.status-legend-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.status-box {
+ width: 15px;
+ height: 15px;
+ border-radius: 3px;
+ display: inline-block;
+}
+
+/* Normal link */
+a {
+ color: lightslategrey;
+}
+
+/* Link when hovered */
+a:hover {
+ color: #9d73eb;
+}
+
+/* Visited link */
+a:visited {
+ color: #8058e3;
+}
+
+/* Active link (while being clicked) */
+a:active {
+ color: #8058e3;
+ /* Custom color when link is clicked */
+}
+
+
+@media (max-width: 1024px) {
+ .summary {
+ flex-direction: column;
+ }
+
+ nav ul {
+ flex-wrap: wrap;
+ }
+
+ nav li {
+ margin-right: 10px;
+ margin-bottom: 5px;
+ }
+}
diff --git a/addons/gdUnit4/src/reporters/html/template/folder_report.html b/addons/gdUnit4/src/reporters/html/template/folder_report.html
new file mode 100644
index 0000000..2cdca67
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/folder_report.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta http-equiv="x-ua-compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>GdUnit4 Testsuite</title>
+ <link rel="stylesheet" href="../css/styles.css">
+ <link href="../css/breadcrumb.css" rel="stylesheet" type="text/css" />
+</head>
+
+<body>
+ <header>
+ <div class="logo">
+ <img src="../css/logo.png" alt="GdUnit4 Logo">
+ <span>GdUnit4</span>
+ </div>
+ <div class="report-container">
+ <h1>Report by Paths</h1>
+ <div>
+ <span class="label">${resource_path}</span>
+ </div>
+ <div class="summary">
+ <div class="summary-item">
+ <span class="label">TestSuites</span>
+ <span class="value">${suite_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Tests</span>
+ <span class="value">${test_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Skipped</span>
+ <span class="value">${skipped_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Flaky</span>
+ <span class="value">${flaky_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Failures</span>
+ <span class="value">${failure_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Orphans</span>
+ <span class="value">${orphan_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Duration</span>
+ <span class="value">${duration}</span>
+ </div>
+ <div class="success-rate">
+ <div class="check-icon status-${report_state}">✓</div>
+ <div class="rate-text">
+ <span class="label">Success Rate</span>
+ <span class="value">${success_percent}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="breadcrumb">
+ <a href="../index.html"><span>All</span></a>
+ <a href="${breadcrumb_path_link}"><span>${path}</span></a>
+ <a class="active" href="#"><span>${testsuite_name}</span></a>
+ </div>
+ </header>
+
+ <main>
+ <div>
+ <div class="tab-report">
+ <div class="grid-item tab">
+ <table id="report-table">
+ <thead>
+ <tr>
+ <th>TestSuites</th>
+ <th>Result</th>
+ <th>Tests</th>
+ <th>Skipped</th>
+ <th>Flaky</th>
+ <th>Failures</th>
+ <th>Orphans</th>
+ <th>Duration</th>
+ <th>Success rate</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${report_table_testsuites}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </main>
+
+ <footer>
+ <p>Generated by <a href="https://github.com/MikeSchulze/gdUnit4">GdUnit4</a> at ${buid_date}</p>
+ <div class="status-legend">
+ <span class="status-legend-item">
+ <span class="status-box status-skipped"></span> Skipped
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-passed"></span> Passed
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-flaky"></span> Flaky
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-warning"></span> Warning
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-failed"></span> Failed
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-error"></span> Error
+ </span>
+ </div>
+ </footer>
+</body>
+
+</html>
diff --git a/addons/gdUnit4/src/reporters/html/template/index.html b/addons/gdUnit4/src/reporters/html/template/index.html
new file mode 100644
index 0000000..342c8f2
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/index.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>GdUnit4 Report</title>
+ <link rel="stylesheet" href="css/styles.css">
+</head>
+
+<body>
+ <header>
+ <div class="logo">
+ <img src="css/logo.png" alt="GdUnit4 Logo">
+ <span>GdUnit4</span>
+ </div>
+ <div class="report-container">
+ <h1>Summary Report</h1>
+ <div class="summary">
+ <div class="summary-item">
+ <span class="label">Test Suites</span>
+ <span class="value">${suite_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Tests</span>
+ <span class="value">${test_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Skipped</span>
+ <span class="value">${skipped_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Flaky</span>
+ <span class="value">${flaky_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Failures</span>
+ <span class="value">${failure_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Orphans</span>
+ <span class="value">${orphan_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Duration</span>
+ <span class="value">${duration}</span>
+ </div>
+ <div class="success-rate">
+ <div class="check-icon status-${report_state}">✓</div>
+ <div class="rate-text">
+ <span class="label">Success Rate</span>
+ <span class="value">${success_percent}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </header>
+ <main>
+ <nav>
+ <ul>
+ <li class="active" data-page="test-suites">TEST SUITES</li>
+ <li data-page="paths">PATHS</li>
+ <li data-page="logging">LOGGING</li>
+ </ul>
+ </nav>
+ <div id="content">
+ <!-- Content will be loaded here based on selected page -->
+ </div>
+ </main>
+
+ <footer>
+ <p>Generated by <a href="https://github.com/MikeSchulze/gdUnit4">GdUnit4</a> at ${buid_date}</p>
+ <div class="status-legend">
+ <span class="status-legend-item">
+ <span class="status-box status-skipped"></span> Skipped
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-passed"></span> Passed
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-flaky"></span> Flaky
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-warning"></span> Warning
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-failed"></span> Failed
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-error"></span> Error
+ </span>
+ </div>
+ </footer>
+
+ <script>
+ // Simple JavaScript to handle page switching
+ document.querySelectorAll('nav li').forEach(item => {
+ item.addEventListener('click', function () {
+ document.querySelectorAll('nav li').forEach(li => li.classList.remove('active'));
+ this.classList.add('active');
+ loadPage(this.getAttribute('data-page'));
+ });
+ });
+
+ function loadPage(page) {
+ if (page === 'test-suites') {
+ document.getElementById('content').innerHTML = `
+ <div class="grid-item tab">
+ <table>
+ <thead>
+ <tr>
+ <th>Test Suites</th>
+ <th>State</th>
+ <th>Tests</th>
+ <th>Skipped</th>
+ <th>Flaky</th>
+ <th>Failures</th>
+ <th>Orphans</th>
+ <th>Duration</th>
+ <th>Quick State</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${report_table_testsuites}
+ </tbody>
+ </table>
+ </div>`
+ } else if (page === 'paths') {
+ document.getElementById('content').innerHTML = `
+ <div class="grid-item tab">
+ <table>
+ <thead>
+ <tr>
+ <th>Paths</th>
+ <th>State</th>
+ <th>Tests</th>
+ <th>Skipped</th>
+ <th>Flaky</th>
+ <th>Failures</th>
+ <th>Orphans</th>
+ <th>Duration</th>
+ <th>Quick State</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${report_table_paths}
+ </tbody>
+ </table>
+ </div>`
+ } else if (page === 'logging') {
+ document.getElementById('content').innerHTML = `
+ <h3>${godot_log_file}</h3>
+ <div class="logging-container">
+ <iframe id="logging_content" src="${log_report}" frameborder="0" height="100%" width="100%"></iframe>
+ </div>`
+ }
+ }
+
+ // Load default page
+ loadPage('test-suites');
+ </script>
+</body>
+
+</html>
diff --git a/addons/gdUnit4/src/reporters/html/template/suite_report.html b/addons/gdUnit4/src/reporters/html/template/suite_report.html
new file mode 100644
index 0000000..4746841
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/html/template/suite_report.html
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta http-equiv="x-ua-compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>GdUnit4 Testsuite</title>
+ <link rel="stylesheet" href="../css/styles.css">
+ <link href="../css/breadcrumb.css" rel="stylesheet" type="text/css" />
+
+ <script src="https://code.jquery.com/jquery-3.6.4.js"></script>
+ <script>
+ $(document).ready(function () {
+ var report_view = $('#report_area').find('.report-column')
+
+ $('#report-table tr').click(function () {
+ var report = $(this).find('.report-column').html();
+ report_view.html(report)
+ $('.selected').removeClass('selected');
+ $(this).addClass('selected');
+ });
+ });
+ </script>
+</head>
+
+<body>
+ <header>
+ <div class="logo">
+ <img src="../css/logo.png" alt="GdUnit4 Logo">
+ <span>GdUnit4</span>
+ </div>
+ <div class="report-container">
+ <h1>Testsuite Report</h1>
+ <div>
+ <span class="label">${resource_path}</span>
+ </div>
+ <div class="summary">
+ <div class="summary-item">
+ <span class="label">Tests</span>
+ <span class="value">${test_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Skipped</span>
+ <span class="value">${skipped_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Flaky</span>
+ <span class="value">${flaky_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Failures</span>
+ <span class="value">${failure_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Orphans</span>
+ <span class="value">${orphan_count}</span>
+ </div>
+ <div class="summary-item">
+ <span class="label">Duration</span>
+ <span class="value">${duration}</span>
+ </div>
+ <div class="success-rate">
+ <div class="check-icon status-${report_state}">✓</div>
+ <div class="rate-text">
+ <span class="label">Success Rate</span>
+ <span class="value">${success_percent}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="breadcrumb">
+ <a href="../index.html"><span>All</span></a>
+ <a href="${breadcrumb_path_link}"><span>${path}</span></a>
+ <a class="active" href="#"><span>${testsuite_name}</span></a>
+ </div>
+ </header>
+
+ <main>
+ <div class="tab-report-grid">
+ <div class="grid-item tab">
+ <table id="report-table">
+ <thead>
+ <tr>
+ <th>Testcase</th>
+ <th>Result</th>
+ <th>Skipped</th>
+ <th>Orphans</th>
+ <th>Duration</th>
+ <th class="report-column">Report</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${report_table_tests}
+ </tbody>
+ </table>
+ </div>
+ <div id="report_area" class="grid-item tab">
+ <h4>Failure Report</h4>
+ <div class="report-column"></div>
+ </div>
+ </div>
+ </main>
+
+ <footer>
+ <p>Generated by <a href="https://github.com/MikeSchulze/gdUnit4">GdUnit4</a> at ${buid_date}</p>
+ <div class="status-legend">
+ <span class="status-legend-item">
+ <span class="status-box status-skipped"></span> Skipped
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-passed"></span> Passed
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-flaky"></span> Flaky
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-warning"></span> Warning
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-failed"></span> Failed
+ </span>
+ <span class="status-legend-item">
+ <span class="status-box status-error"></span> Error
+ </span>
+ </div>
+ </footer>
+</body>
+
+<script>
+ function groupTableRows() {
+ const table = document.getElementById('report-table');
+ const rows = table.querySelectorAll('tbody tr');
+
+ let previousTestCase = '';
+ let groupStartIndex = 0;
+ let groupColorToggle = true; // Toggle between two colors
+
+ rows.forEach((row, index) => {
+ const testCaseName = row.cells[0].textContent.trim();
+
+ if (testCaseName !== previousTestCase) {
+ // Close the previous group
+ if (index > 0 && groupStartIndex !== index - 1) {
+ // Apply background color to the group
+ const groupClass = groupColorToggle ? 'group-bg-1' : 'group-bg-2';
+ for (let i = groupStartIndex; i <= index - 1; i++) {
+ rows[i].classList.add(groupClass);
+ }
+
+ // Toggle background color for the next group
+ groupColorToggle = !groupColorToggle;
+ }
+
+ // Start a new group
+ groupStartIndex = index;
+ }
+
+ previousTestCase = testCaseName;
+ });
+
+ // Handle the last group
+ if (groupStartIndex !== rows.length - 1) {
+ // Apply background color to the last group
+ const groupClass = groupColorToggle ? 'group-bg-1' : 'group-bg-2';
+ for (let i = groupStartIndex; i < rows.length; i++) {
+ rows[i].classList.add(groupClass);
+ }
+ }
+ }
+
+ // Call the function to group table rows after the DOM loads
+ document.addEventListener('DOMContentLoaded', groupTableRows);
+</script>
+
+</html>
diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd
new file mode 100644
index 0000000..0b9674f
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd
@@ -0,0 +1,143 @@
+# This class implements the JUnit XML file format
+# based checked https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd
+class_name JUnitXmlReportWriter
+extends GdUnitReportWriter
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+const ATTR_CLASSNAME := "classname"
+const ATTR_ERRORS := "errors"
+const ATTR_FAILURES := "failures"
+const ATTR_HOST := "hostname"
+const ATTR_ID := "id"
+const ATTR_MESSAGE := "message"
+const ATTR_NAME := "name"
+const ATTR_PACKAGE := "package"
+const ATTR_SKIPPED := "skipped"
+const ATTR_FLAKY := "flaky"
+const ATTR_TESTS := "tests"
+const ATTR_TIME := "time"
+const ATTR_TIMESTAMP := "timestamp"
+const ATTR_TYPE := "type"
+
+const HEADER := '<?xml version="1.0" encoding="UTF-8" ?>\n'
+
+
+func output_format() -> String:
+ return "XML"
+
+
+func write(report_path: String, report: GdUnitReportSummary) -> String:
+ var result_file: String = "%s/results.xml" % report_path
+ DirAccess.make_dir_recursive_absolute(report_path)
+ var file := FileAccess.open(result_file, FileAccess.WRITE)
+ if file == null:
+ push_warning("Can't saving the result to '%s'\n Error: %s" % [result_file, error_string(FileAccess.get_open_error())])
+ else:
+ file.store_string(build_junit_report(report_path, report))
+ return result_file
+
+
+func build_junit_report(report_path: String, report: GdUnitReportSummary) -> String:
+ var iso8601_datetime := Time.get_date_string_from_system()
+ var test_suites := XmlElement.new("testsuites")\
+ .attribute(ATTR_ID, iso8601_datetime)\
+ .attribute(ATTR_NAME, report_path.get_file())\
+ .attribute(ATTR_TESTS, report.test_count())\
+ .attribute(ATTR_FAILURES, report.failure_count())\
+ .attribute(ATTR_SKIPPED, report.skipped_count())\
+ .attribute(ATTR_FLAKY, report.flaky_count())\
+ .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\
+ .add_childs(build_test_suites(report))
+ var as_string := test_suites.to_xml()
+ test_suites.dispose()
+ return HEADER + as_string
+
+
+func build_test_suites(summary: GdUnitReportSummary) -> Array:
+ var test_suites: Array[XmlElement] = []
+ for index in summary.get_reports().size():
+ var suite_report :GdUnitTestSuiteReport = summary.get_reports()[index]
+ var iso8601_datetime := Time.get_datetime_string_from_unix_time(suite_report.time_stamp())
+ test_suites.append(XmlElement.new("testsuite")\
+ .attribute(ATTR_ID, index)\
+ .attribute(ATTR_NAME, suite_report.name())\
+ .attribute(ATTR_PACKAGE, suite_report.path())\
+ .attribute(ATTR_TIMESTAMP, iso8601_datetime)\
+ .attribute(ATTR_HOST, "localhost")\
+ .attribute(ATTR_TESTS, suite_report.test_count())\
+ .attribute(ATTR_FAILURES, suite_report.failure_count())\
+ .attribute(ATTR_ERRORS, suite_report.error_count())\
+ .attribute(ATTR_SKIPPED, suite_report.skipped_count())\
+ .attribute(ATTR_FLAKY, suite_report.flaky_count())\
+ .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(suite_report.duration()))\
+ .add_childs(build_test_cases(suite_report)))
+ return test_suites
+
+
+func build_test_cases(suite_report: GdUnitTestSuiteReport) -> Array:
+ var test_cases: Array[XmlElement] = []
+ for index in suite_report.get_reports().size():
+ var report :GdUnitTestCaseReport = suite_report.get_reports()[index]
+ test_cases.append( XmlElement.new("testcase")\
+ .attribute(ATTR_NAME, JUnitXmlReportWriter.encode_xml(report.name()))\
+ .attribute(ATTR_CLASSNAME, report.suite_name())\
+ .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\
+ .add_childs(build_reports(report)))
+ return test_cases
+
+
+func build_reports(test_report: GdUnitTestCaseReport) -> Array:
+ var failure_reports: Array[XmlElement] = []
+
+ for report: GdUnitReport in test_report.get_test_reports():
+ if report.is_failure():
+ failure_reports.append(XmlElement.new("failure")\
+ .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\
+ .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\
+ .text(convert_rtf_to_text(report.message())))
+ elif report.is_error():
+ failure_reports.append(XmlElement.new("error")\
+ .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report.get_resource_path(), report.line_number()])\
+ .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\
+ .text(convert_rtf_to_text(report.message())))
+ elif report.is_skipped():
+ failure_reports.append(XmlElement.new("skipped")\
+ .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\
+ .text(convert_rtf_to_text(report.message())))
+ return failure_reports
+
+
+func convert_rtf_to_text(bbcode: String) -> String:
+ return GdUnitTools.richtext_normalize(bbcode)
+
+
+static func to_type(type: int) -> String:
+ match type:
+ GdUnitReport.SUCCESS:
+ return "SUCCESS"
+ GdUnitReport.WARN:
+ return "WARN"
+ GdUnitReport.FAILURE:
+ return "FAILURE"
+ GdUnitReport.ORPHAN:
+ return "ORPHAN"
+ GdUnitReport.TERMINATED:
+ return "TERMINATED"
+ GdUnitReport.INTERUPTED:
+ return "INTERUPTED"
+ GdUnitReport.ABORT:
+ return "ABORT"
+ return "UNKNOWN"
+
+
+static func to_time(duration: int) -> String:
+ return "%4.03f" % (duration / 1000.0)
+
+
+static func encode_xml(value: String) -> String:
+ return value.xml_escape(true)
+
+
+#static func to_ISO8601_datetime() -> String:
+ #return "%04d-%02d-%02dT%02d:%02d:%02d" % [date["year"], date["month"], date["day"], date["hour"], date["minute"], date["second"]]
diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid
new file mode 100644
index 0000000..16ffa18
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid
@@ -0,0 +1 @@
+uid://bf8g04wtq4lfp
diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd b/addons/gdUnit4/src/reporters/xml/XmlElement.gd
new file mode 100644
index 0000000..86c7421
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd
@@ -0,0 +1,69 @@
+class_name XmlElement
+extends RefCounted
+
+var _name :String
+# Dictionary[String, String]
+var _attributes :Dictionary = {}
+var _childs :Array[XmlElement] = []
+var _parent :XmlElement = null
+var _text :String = ""
+
+
+func _init(name :String) -> void:
+ _name = name
+
+
+func dispose() -> void:
+ for child in _childs:
+ child.dispose()
+ _childs.clear()
+ _attributes.clear()
+ _parent = null
+
+
+func attribute(name :String, value :Variant) -> XmlElement:
+ _attributes[name] = str(value)
+ return self
+
+
+func text(p_text :String) -> XmlElement:
+ _text = p_text if p_text.ends_with("\n") else p_text + "\n"
+ return self
+
+
+func add_child(child :XmlElement) -> XmlElement:
+ _childs.append(child)
+ child._parent = self
+ return self
+
+
+func add_childs(childs :Array[XmlElement]) -> XmlElement:
+ for child in childs:
+ @warning_ignore("return_value_discarded")
+ add_child(child)
+ return self
+
+
+func indentation() -> String:
+ return "" if _parent == null else _parent.indentation() + " "
+
+
+func to_xml() -> String:
+ var attributes := ""
+ for key in _attributes.keys() as Array[String]:
+ attributes += ' {attr}="{value}"'.format({"attr": key, "value": _attributes.get(key)})
+
+ var childs := ""
+ for child in _childs:
+ childs += child.to_xml()
+
+ return "{_indentation}<{name}{attributes}>\n{childs}{text}{_indentation}</{name}>\n"\
+ .format({"name": _name,
+ "attributes": attributes,
+ "childs": childs,
+ "_indentation": indentation(),
+ "text": cdata(_text)})
+
+
+func cdata(p_text :String) -> String:
+ return "" if p_text.is_empty() else "<![CDATA[\n{text}]]>\n".format({"text" : p_text})
diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid
new file mode 100644
index 0000000..f02b6a2
--- /dev/null
+++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid
@@ -0,0 +1 @@
+uid://bae36annbua3k