Files
gplan-2/game/probe/probe_coverage_recorder.gd
T
DKChan 2cbd2ca9a7 批1(QA harness·探针存量emit真输入化+覆盖登记写入): 两探针52处emit站点转真输入管线(push_input press+release成对)51处,保留1处置灰态穿透emit标〔接线断言〕; 新增probe_coverage_recorder.gd真输入覆盖登记件(旗标在位期间命中入账,锚链归一化键=具名/脚本锚+类#钮面文本+序位,@临时名不入键,JSONP落tests/.gates/coverage/每探针一件); probe_ui_util补coverage_hook单点+push_motion/push_wheel(滚轮press+release成对——只发press钉死鼠标焦点实证fork/返回营地两钮点击全失效)/window_panel/scroll_button_into_view(折叠区行钮rect越窗底栏实证误中返回营地钮,真输入滚轮滚入可视区再点); V5b遮挡试探订正=前窗经真实返回钮清账再真点开窗(emit直发穿遮罩属口径违例); 落点判定补钉2处(esc段子界面开窗落点/树窗返回钮落点); test-conventions §2新增UI驱动断言真输入管线条文(滚轮成对+折叠区滚入+落点判定+接线断言豁免口径); .gates/.gitignore收coverage/。读数: screen_flow_probe孤立直跑PASS(六步动线+屏级互斥全绿); ui_click_probe孤立直跑exit=0问题=0(budget=15210,三笔树购买请求全通,PROBE_SEED=7); 登记落盘ui_click=27控件/38命中/临时名跳过0,screen_flow=19控件/25命中。全量档闸(fd9c8ad9基线): 两新增红均判为主区推进漂移非本批引入——流水线闸红=待办清单QA条目证据句(master 1730a8fb已修),单元settlement_wiring_snapshot红待并master后窄跑复核; 本步凭据=两探针直跑绿+偏差8条已回写qa-harness讨论文档§10
2026-11-06 01:50:50 +08:00

208 lines
8.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# probe_coverage_recorder.gd — 真输入覆盖登记件(探针共用面)
# 是什么:探针/C 驱动件把合成 InputEvent 经 Viewport.push_input 送进引擎输入管线时,登记
# 「真输入实际命中了哪些交互控件」,探针收尾落 JSONP 式 .js 到 tests/.gates/coverage/
# (.js 供 file:// 页面 <script src> 直载——fetch 本地 json 会被 CORS 拦)。
# 成员面(两源):① 场景树全部 BaseButton——pressed 沿自动接线(重扫懒触发,见下);
# ② 自绘命中类——现役接 camp_scene_view.object_pressed(信号带物件 id,key 追加 #物件id 后缀),
# 其余自绘命中面走 note_hit() 显式口。
# 真输入旗标:push 前置位、帧末清位——只有旗标在位期间的命中入账;信号直发(〔接线断言〕保留站)
# 与产品码自发射永不入账。命中=控件真的收到事件:hitbox 错位/遮挡时零命中,缺口自然暴露——
# 这是「新控件落地→覆盖登记自动出现它」的数据源。
# key 规则(契约「场景文件路径+节点路径」的实况落地偏差,详见 qa-harness 讨论文档回写节):
# 本仓库 UI 大头由代码构建(main_ui_build/ui_kit 不设节点名),运行时节点路径全为 @类@序
# 临时名——契约原样落地产出空登记。故键取「锚链归一化」形态:自主根到控件只收集带稳定身份的
# 锚(具名节点用其名、带脚本节点用脚本文件名;@ 临时名段整体不入键),控件本体记 类#钮面文本,
# 同键控件按接线遍历序追加 #序位。键在确定构建序下可复现;UI 构建序变更会平移同键序位——
# 表现为键漂移而非静默丢失,属测量面可接受代价。
# 用法:探针 _ready 里 `_recorder = ProbeCoverageRecorder.new()` → `attach(main, "<探针名>")`;
# 收尾 `dump_js()`。探针共用面不入 S 系扫描面(code-conventions §4.13 分档)。
extends RefCounted
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
var _main: Node
var _probe_name: String
var _pending := false # 真输入在途旗标
var _camp: Control = null # 营地场景根(自绘命中单点)
var _hits: Dictionary = {} # 稳定 key(String) → 命中次数(int)
var _node_keys: Dictionary = {} # 接线时分配的节点键(instance_id → key)
var _key_seen: Dictionary = {} # 基键出现次数(同键控件按遍历序追加序位)
var _wired: Dictionary = {} # 已连 pressed 的按钮实例 id → true(防重连)
var _skipped_temp := 0 # 临时名命中笔数(不入 key,只计数)
# ---- 装载与接线 ----
func attach(main: Node, probe_name: String) -> void:
_main = main
_probe_name = probe_name
ProbeUiUtil.coverage_hook = self # 共用件真输入口统一过旗标(push_click/push_click_at/push_motion)
_wire_buttons()
_wire_camp_scene()
# 懒重扫:每次真输入 push 前接线一遍新入树的按钮(开层重建的钮行在下次点击前接上);
# 已释放按钮的连接随节点释放失效,登记表按 instance_id 清扫。
func mark_real_input() -> void:
for id in _wired.keys():
if instance_from_id(int(id)) == null:
_wired.erase(id)
_node_keys.erase(id)
_wire_buttons()
if not _pending: # 同帧多次 push 只挂一次帧末清位旗(重复 connect 会报引擎错)
_pending = true
_main.get_tree().process_frame.connect(_clear_pending, CONNECT_ONE_SHOT)
func _clear_pending() -> void:
_pending = false
func _wire_buttons() -> void:
_wire_into(_main)
func _wire_into(node: Node) -> void:
if node is BaseButton:
var id := node.get_instance_id()
if not _wired.has(id):
_wired[id] = true
_node_keys[id] = _assign_key(node)
(node as BaseButton).pressed.connect(_on_button_pressed.bind(id))
for child in node.get_children():
_wire_into(child)
func _wire_camp_scene() -> void:
_camp = _find_by_script(_main, "camp_scene_view")
if _camp != null:
_camp.object_pressed.connect(_on_camp_object)
func _find_by_script(root: Node, script_name: String) -> Control:
var s: Script = root.get_script()
if s != null and String(s.resource_path).contains(script_name) and root is Control:
return root as Control
for child in root.get_children():
var found := _find_by_script(child, script_name)
if found != null:
return found
return null
# ---- 键分配(锚链归一化,接线时一次算好存表)----
func _assign_key(n: Node) -> String:
var chain := _anchor_chain(n)
var leaf := ""
if n is BaseButton:
leaf = "%s#%s" % [n.get_class(), String((n as BaseButton).text)]
else:
leaf = n.get_class()
var base := "%s:%s/%s" % [String(_main.scene_file_path), "/".join(chain), leaf]
# 同键控件按接线遍历序加序位(#1 省略)——重复键可分账,序位随构建序漂移属已知代价
_key_seen[base] = int(_key_seen.get(base, 0)) + 1
if int(_key_seen[base]) > 1:
base += "#%d" % int(_key_seen[base])
return base
# 自主根到 n 的稳定锚链:具名节点取其名、带脚本节点取脚本文件名;@ 临时名段不入链。
func _anchor_chain(n: Node) -> Array[String]:
var chain: Array[String] = []
var cur: Node = n
while cur != null and cur != _main:
var nm := String(cur.name)
if not nm.begins_with("@"):
chain.push_front(nm)
elif cur.get_script() != null:
var sp := String((cur.get_script() as Script).resource_path)
chain.push_front(sp.get_file().trim_suffix(".gd"))
cur = cur.get_parent()
if chain.is_empty():
chain.push_front("(root)")
return chain
# ---- 显式登记口(自绘命中类无信号可连时的单点)----
func note_hit(n: Node, suffix: String = "") -> void:
if not _pending or n == null:
return
var id := n.get_instance_id()
var key := String(_node_keys.get(id, ""))
if key == "":
key = _assign_key(n)
_node_keys[id] = key
_bump(key + suffix)
# ---- 命中处理 ----
func _on_button_pressed(id: int) -> void:
if not _pending:
return
var key := String(_node_keys.get(id, ""))
if key == "":
_skipped_temp += 1
return
_bump(key)
func _on_camp_object(key: StringName) -> void:
if not _pending or _camp == null:
return
var id := _camp.get_instance_id()
var base := String(_node_keys.get(id, ""))
if base == "":
base = _assign_key(_camp)
_node_keys[id] = base
_bump(base + "#" + String(key))
func _bump(key: String) -> void:
_hits[key] = int(_hits.get(key, 0)) + 1
# ---- 落盘(JSONP 式 .js——file:// 页面 <script src> 直载)----
func dump_js() -> void:
var dir := DirAccess.open("res://")
if dir == null:
push_error("[coverage] res:// 打不开,登记未落盘")
return
var mk := dir.make_dir_recursive("tests/.gates/coverage")
if mk != OK and not dir.dir_exists("tests/.gates/coverage"):
push_error("[coverage] coverage 目录建不了(err=%d),登记未落盘" % mk)
return
var path := "res://tests/.gates/coverage/probe_hits_%s.js" % _probe_name
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
push_error("[coverage] 登记文件打不开:%s" % path)
return
var total := 0
for k in _hits:
total += int(_hits[k])
var payload := {
"meta": {
"probe": _probe_name,
"seed": OS.get_environment("PROBE_SEED"),
"scene": String(_main.scene_file_path),
"generated_at": Time.get_datetime_string_from_system(),
"driver": "InputEvent via Viewport.push_input(真输入管线)",
"key_shape": "锚链归一化(具名/脚本锚+类#文本+序位;@ 临时名不入键)",
"hits_total": total,
"skipped_temp_names": _skipped_temp,
},
"hits": _hits,
}
var js := "/* 真输入覆盖登记——探针每次运行自动覆写,人工修订无效(豁免面走人工清单,不落本文件)。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.probe_real_input = window.__QA_COVERAGE__.probe_real_input || {};\n"
js += "window.__QA_COVERAGE__.probe_real_input[%s] = %s;\n" % [
JSON.stringify(_probe_name), JSON.stringify(payload, " ")]
f.store_string(js)
f.close()
print("[coverage] 真输入命中登记已落盘 %s(控件 %d 处 / 命中 %d 次 / 临时名跳过 %d)" % [
path, _hits.size(), total, _skipped_temp])