Files
mtgodot-poc/project/testing/playable_report.gd
T

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