Implement playable Mac client and rendering validation
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
Reference in New Issue
Block a user