setting up GDUnit
Some checks failed
Create tag and build when new code gets to main / Export (push) Failing after 3m40s

This commit is contained in:
2026-01-25 18:19:26 +01:00
parent 39d6ab1c5f
commit c28d97de2d
471 changed files with 29716 additions and 16 deletions

View File

@@ -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 ""

View File

@@ -0,0 +1 @@
uid://meit1uha85vb

View File

@@ -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 ""

View File

@@ -0,0 +1 @@
uid://bapou3o42rua2

View File

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

View File

@@ -0,0 +1 @@
uid://d34dh6aril014

View File

@@ -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"]

View File

@@ -0,0 +1 @@
uid://cd8snijgldveh

View File

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

View File

@@ -0,0 +1 @@
uid://buoo3f5s15ptt

View File

@@ -0,0 +1,234 @@
@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.color(Color.DARK_SALMON) \
.style(GdUnitMessageWriter.BOLD) \
.println_message(event.suite_name() + ":finalze")
_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()
):
_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()

View File

@@ -0,0 +1 @@
uid://cnc3bdrbur61g

View File

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

View File

@@ -0,0 +1 @@
uid://byxm8mid1s1ev

View File

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

View File

@@ -0,0 +1 @@
uid://dndrqodnpdtx0

View File

@@ -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]

View File

@@ -0,0 +1 @@
uid://dc1daqsf13p2m

View File

@@ -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;
}

BIN
addons/gdUnit4/src/reporters/html/template/css/logo.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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"]]

View File

@@ -0,0 +1 @@
uid://bxwmuqci2eaj1

View File

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

View File

@@ -0,0 +1 @@
uid://byogn2u5815e0