126 lines
4.0 KiB
GDScript
126 lines
4.0 KiB
GDScript
class_name PlayableReport
|
|
|
|
## 联网测试进程内的报告构建器。
|
|
## 只写非敏感的 client-report.json;最终 process/退出日志门禁由父脚本补齐。
|
|
|
|
const SCHEMA_VERSION := 1
|
|
const VALID_STATUSES := ["PASS", "FAIL", "BLOCKED", "SKIP"]
|
|
|
|
var _report: Dictionary = {}
|
|
var _started_us := 0
|
|
|
|
func begin(run_id: String, suite: String, config: Dictionary) -> void:
|
|
_started_us = Time.get_ticks_usec()
|
|
_report = {
|
|
"schema_version": SCHEMA_VERSION,
|
|
"run_id": run_id,
|
|
"suite": suite,
|
|
"status": "BLOCKED",
|
|
"build": _build_info(),
|
|
"environment": _environment_info(),
|
|
"cases": [],
|
|
"events": "events.jsonl",
|
|
"failures": [],
|
|
"blocked": [],
|
|
"coverage": {"required": 0, "passed": 0},
|
|
"exit_gate": {"checked": false, "process_code": null, "errors": []},
|
|
}
|
|
# Keep configuration useful for reproducing a run without copying any secret.
|
|
_report["scenario_id"] = String(config.get("scenario_id", ""))
|
|
|
|
func add_case(case_id: String, status: String, reason := "", evidence: Array = [], duration_ms := 0.0) -> void:
|
|
var normalized := status.to_upper()
|
|
if not VALID_STATUSES.has(normalized):
|
|
normalized = "FAIL"
|
|
_fail("invalid case status for %s" % case_id)
|
|
var item := {"id": case_id, "status": normalized, "duration_ms": duration_ms,
|
|
"reason": reason, "evidence": _sanitize(evidence)}
|
|
_report.cases.append(item)
|
|
_report.coverage.required = int(_report.coverage.required) + 1
|
|
if normalized == "PASS":
|
|
_report.coverage.passed = int(_report.coverage.passed) + 1
|
|
elif normalized == "FAIL":
|
|
_fail("%s: %s" % [case_id, reason if not reason.is_empty() else "failed"])
|
|
elif normalized == "BLOCKED":
|
|
_report.blocked.append(reason if not reason.is_empty() else case_id)
|
|
|
|
func add_event(event: Dictionary, events_path: String) -> void:
|
|
var safe: Variant = _sanitize(event)
|
|
var file := FileAccess.open(events_path, FileAccess.READ_WRITE)
|
|
if file == null:
|
|
file = FileAccess.open(events_path, FileAccess.WRITE)
|
|
if file == null:
|
|
_fail("cannot open events file")
|
|
return
|
|
file.seek_end()
|
|
file.store_line(JSON.stringify(safe))
|
|
file.close()
|
|
|
|
func finish() -> Dictionary:
|
|
if _report.is_empty():
|
|
return {}
|
|
var required := int(_report.coverage.required)
|
|
var passed := int(_report.coverage.passed)
|
|
var failures: Array = _report.failures
|
|
if not failures.is_empty():
|
|
_report.status = "FAIL"
|
|
elif required == 0 or passed != required or not _report.blocked.is_empty():
|
|
_report.status = "BLOCKED"
|
|
else:
|
|
_report.status = "PASS"
|
|
_report.duration_seconds = float(Time.get_ticks_usec() - _started_us) / 1000000.0
|
|
return _report
|
|
|
|
func write(path: String) -> bool:
|
|
var report := finish()
|
|
if report.is_empty():
|
|
return false
|
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
if file == null:
|
|
return false
|
|
file.store_string(JSON.stringify(report, " "))
|
|
file.close()
|
|
return true
|
|
|
|
func _fail(message: String) -> void:
|
|
_report.failures.append(message)
|
|
|
|
func _build_info() -> Dictionary:
|
|
return {
|
|
"engine_sha256": OS.get_environment("MT_BUILD_ENGINE_SHA256"),
|
|
"extension_sha256": OS.get_environment("MT_BUILD_EXTENSION_SHA256"),
|
|
"pck_sha256": OS.get_environment("MT_BUILD_PCK_SHA256"),
|
|
"arch": OS.get_environment("MT_BUILD_ARCH"),
|
|
}
|
|
|
|
func _environment_info() -> Dictionary:
|
|
var size := DisplayServer.window_get_size() if DisplayServer.get_name() != "headless" else Vector2i.ZERO
|
|
return {
|
|
"os": OS.get_name(),
|
|
"renderer": RenderingServer.get_video_adapter_name(),
|
|
"resolution": [size.x, size.y],
|
|
}
|
|
|
|
func _sanitize(value: Variant) -> Variant:
|
|
if value is Dictionary:
|
|
var out := {}
|
|
for key in value:
|
|
var name := String(key).to_lower()
|
|
if name in ["password", "passwd", "token", "secret", "account"]:
|
|
out[key] = "[redacted]"
|
|
else:
|
|
out[key] = _sanitize(value[key])
|
|
return out
|
|
if value is Array:
|
|
var out_array: Array = []
|
|
for item in value:
|
|
out_array.append(_sanitize(item))
|
|
return out_array
|
|
if value is Vector2:
|
|
return [value.x, value.y]
|
|
if value is Vector3:
|
|
return [value.x, value.y, value.z]
|
|
if value is Object:
|
|
return "[object]"
|
|
return value
|