@harmonyos-arkts/d2h 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -44
- package/agents/android-to-hmos-00-orchestrator.md +34 -28
- package/agents/d2h.md +266 -95
- package/dist/bin/install-opencode.js +137 -43
- package/dist/src/install-opencode.js +44 -0
- package/dist/src/install-vscode.js +226 -0
- package/dist/src/toolchain.js +83 -0
- package/docs/environment-check.md +86 -0
- package/opencode/agents.json +1 -1
- package/opencode/vscode-d2h-package.json +7 -0
- package/opencode/vscode-d2h-plugin.mjs +29 -0
- package/package.json +6 -4
- package/schemas/task-envelope.schema.json +58 -4
- package/schemas/task-plan.schema.json +281 -63
- package/scripts/check-environment.mjs +321 -0
- package/skills/android2hmos_resources_convert/SKILL.md +37 -19
- package/skills/appgraph-rule-audit/SKILL.md +24 -5
- package/skills/d2h-hypium-forge/SKILL.md +252 -0
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -0
- package/skills/d2h-hypium-forge/references/official-api.md +135 -0
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -0
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -0
- package/skills/d2h-hypium-forge/scripts/_common_template.py +614 -0
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +156 -0
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -0
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -0
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -0
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -0
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -0
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -0
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -0
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -0
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -0
- package/skills/d2h-hypium-forge/scripts/run_direct.py +392 -0
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -0
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -0
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -0
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -0
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -0
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -0
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -0
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -0
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -0
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -0
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -0
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -0
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -0
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -0
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -0
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -0
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -0
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -0
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -0
- package/skills/d2h-utg-static-generate/SKILL.md +73 -0
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -0
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -0
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -0
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -0
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -0
- package/skills/harmonyos-migration-playbook/SKILL.md +30 -3
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +4 -4
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +4 -3
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +2 -2
- package/skills/maintaining-migration-report/SKILL.md +88 -99
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -0
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -0
- package/skills/managing-d2h-toolchain/SKILL.md +23 -0
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -0
- package/skills/managing-d2h-toolchain/toolchain.json +16 -0
- package/skills/preparing-migration-workspace/SKILL.md +76 -69
- package/skills/preparing-migration-workspace/toolchain.json +0 -43
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""forge_report.py — 每工程测试执行报告(HTML,参考 Allure/extent 信息架构,零依赖)。
|
|
4
|
+
|
|
5
|
+
数据源(.hypium_forge/ 下):
|
|
6
|
+
results.json 最新一轮执行结果(case/status/msg/dur)
|
|
7
|
+
cases.json 用例(含 priority/scenarios/steps/title)
|
|
8
|
+
scenarios.json 场景注册表(可选,有则出场景维度)
|
|
9
|
+
reports/<d>/<ts>/ 截图:<case>.jpeg(成功)/ <case>_fail.jpeg(失败)
|
|
10
|
+
|
|
11
|
+
产出:test-report.html
|
|
12
|
+
① 执行概况:总数/通过率/优先级分布/耗时统计/场景覆盖
|
|
13
|
+
② 用例明细卡片:状态徽标、优先级、场景、步骤、失败原因、耗时、成功&失败截图
|
|
14
|
+
③ 结果条形总览(纯 CSS,按用例序排列)
|
|
15
|
+
|
|
16
|
+
用法(在 .hypium_forge 目录下):
|
|
17
|
+
python3 forge_report.py [--run-dir reports/2026-09-27/10-04-05] # 不指定则取最新
|
|
18
|
+
"""
|
|
19
|
+
import argparse
|
|
20
|
+
import glob
|
|
21
|
+
import html
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
EMBED = False # --embed:关键帧 base64 内嵌开关
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def thumb_b64(path, max_w=480):
|
|
31
|
+
"""sips 缩略 → base64(macOS 零依赖;失败返回 None 走原相对路径)。"""
|
|
32
|
+
import base64 as _b64
|
|
33
|
+
import subprocess as _sp
|
|
34
|
+
import tempfile as _tf
|
|
35
|
+
try:
|
|
36
|
+
with _tf.NamedTemporaryFile(suffix='.jpeg', delete=False) as tf:
|
|
37
|
+
tmp = tf.name
|
|
38
|
+
_sp.run(['sips', '-Z', str(max_w), '-s', 'formatOptions', '55', path, '--out', tmp],
|
|
39
|
+
capture_output=True, timeout=15)
|
|
40
|
+
data = open(tmp, 'rb').read()
|
|
41
|
+
import os as _os
|
|
42
|
+
_os.unlink(tmp)
|
|
43
|
+
return 'data:image/jpeg;base64,' + _b64.b64encode(data).decode()
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load(path, default=None):
|
|
49
|
+
if os.path.exists(path):
|
|
50
|
+
with open(path, encoding='utf-8') as f:
|
|
51
|
+
return json.load(f)
|
|
52
|
+
return default
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_latest_run():
|
|
56
|
+
runs = sorted(glob.glob(os.path.join('reports', '*', '*')))
|
|
57
|
+
dirs = [r for r in runs if os.path.isdir(r) and
|
|
58
|
+
(glob.glob(os.path.join(r, '*.jpeg')) or glob.glob(os.path.join(r, '*.jpg')) or
|
|
59
|
+
glob.glob(os.path.join(r, '*.png')))]
|
|
60
|
+
return dirs[-1] if dirs else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__JS__ = '<script>var btns=document.querySelectorAll(".filters button");var f={st:"",pr:""};btns.forEach(function(b){b.onclick=function(){document.querySelectorAll(\'[data-f="\'+b.dataset.f+\'"]\').forEach(function(x){x.classList.remove("on")});b.classList.add("on");f[b.dataset.f]=b.dataset.v;apply();};});document.getElementById("q").oninput=apply;function apply(){var q=document.getElementById("q").value.toLowerCase();document.querySelectorAll("tr.row").forEach(function(tr){var ok=(!f.st||tr.dataset.st===f.st)&&(!f.pr||tr.dataset.pr===f.pr)&&(!q||tr.dataset.kw.indexOf(q)>=0);tr.classList.toggle("hide",!ok);});}</script>'
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
ap = argparse.ArgumentParser()
|
|
67
|
+
ap.add_argument('--run-dir', default=None)
|
|
68
|
+
ap.add_argument('--out', default='test-report.html')
|
|
69
|
+
ap.add_argument('--embed', action='store_true', help='关键帧缩略图 base64 内嵌(单文件可分发)')
|
|
70
|
+
args = ap.parse_args()
|
|
71
|
+
|
|
72
|
+
global EMBED
|
|
73
|
+
EMBED = args.embed
|
|
74
|
+
cases = {c['id']: c for c in load('cases.json', [])}
|
|
75
|
+
if not cases:
|
|
76
|
+
import re as _re
|
|
77
|
+
for pf in glob.glob(os.path.join('testcases', 'T[CP]*.py')):
|
|
78
|
+
try:
|
|
79
|
+
src = open(pf, encoding='utf-8', errors='replace').read()
|
|
80
|
+
except Exception:
|
|
81
|
+
continue
|
|
82
|
+
mid = os.path.splitext(os.path.basename(pf))[0]
|
|
83
|
+
base = mid.split('_')[0]
|
|
84
|
+
m = _re.search(r'\"\"\"(.+?)\"\"\"', src, _re.S)
|
|
85
|
+
title = mid
|
|
86
|
+
if m:
|
|
87
|
+
title = m.group(1).strip().splitlines()[0][:50]
|
|
88
|
+
cases[base] = {'id': base, 'title': title, 'priority': '未分级',
|
|
89
|
+
'detail': (m.group(1).strip()[:600] if m else '')}
|
|
90
|
+
results = load('results.json', [])
|
|
91
|
+
scenarios = {s['id']: s for s in load('scenarios.json', [])}
|
|
92
|
+
run_dir = args.run_dir or find_latest_run()
|
|
93
|
+
|
|
94
|
+
# 结果 → 用例 id 归一(类名带 slug 后缀)
|
|
95
|
+
norm = []
|
|
96
|
+
for r in results:
|
|
97
|
+
rc = r.get('case', '')
|
|
98
|
+
cid = max((c for c in cases if rc == c or rc.startswith(c + '_')), key=len, default=rc)
|
|
99
|
+
norm.append((cid, r))
|
|
100
|
+
|
|
101
|
+
n = len(norm)
|
|
102
|
+
p = sum(1 for _, r in norm if r.get('status') == 'PASS')
|
|
103
|
+
fails = sum(1 for _, r in norm if r.get('status') == 'FAIL')
|
|
104
|
+
durs = [r.get('dur', 0) for _, r in norm]
|
|
105
|
+
total_dur = sum(durs)
|
|
106
|
+
|
|
107
|
+
pr_stat = {}
|
|
108
|
+
for cid, r in norm:
|
|
109
|
+
pr = cases.get(cid, {}).get('priority', '未分级')
|
|
110
|
+
pr_stat.setdefault(pr, [0, 0])
|
|
111
|
+
pr_stat[pr][0] += 1
|
|
112
|
+
pr_stat[pr][1] += 1 if r.get('status') == 'PASS' else 0
|
|
113
|
+
|
|
114
|
+
cov = {}
|
|
115
|
+
for cid, c in cases.items():
|
|
116
|
+
for fs in c.get('scenarios', []):
|
|
117
|
+
cov[fs] = cov.get(fs, 0) + 1
|
|
118
|
+
|
|
119
|
+
def shot(cid, rc='', suffix=''):
|
|
120
|
+
"""按 类名(rc) → 用例id 逐级匹配截图文件(类名带 slug 后缀,如 TC01_case/TC05_webview)"""
|
|
121
|
+
if not run_dir:
|
|
122
|
+
return None
|
|
123
|
+
for stem in (rc, cid):
|
|
124
|
+
for ext in ('.jpeg', '.jpg', '.png'):
|
|
125
|
+
f = os.path.join(run_dir, stem + suffix + ext)
|
|
126
|
+
if os.path.exists(f):
|
|
127
|
+
return os.path.relpath(f)
|
|
128
|
+
# 前缀兜底:TC05_webview 类名带 slug
|
|
129
|
+
pats = [os.path.join(run_dir, f'{cid}_*{suffix}.jpeg'), os.path.join(run_dir, f'{cid}_*{suffix}.jpg')]
|
|
130
|
+
for pat in pats:
|
|
131
|
+
g = sorted(glob.glob(pat))
|
|
132
|
+
if g:
|
|
133
|
+
return os.path.relpath(g[0])
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
css = """
|
|
137
|
+
:root{--bg:#f4f6f9;--card:#fff;--ink:#20242c;--sub:#68707e;--line:#e5e8ee;
|
|
138
|
+
--ok:#0e9f6e;--bad:#e02424;--key:#3b6fd4}
|
|
139
|
+
*{box-sizing:border-box}
|
|
140
|
+
body{margin:0;font:14.5px/1.6 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--ink)}
|
|
141
|
+
.wrap{max-width:1100px;margin:0 auto;padding:28px 20px 70px}
|
|
142
|
+
h1{font-size:23px;margin:.2em 0 .2em}
|
|
143
|
+
.meta{color:var(--sub);font-size:12.5px}
|
|
144
|
+
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:18px 0}
|
|
145
|
+
.kpi{background:var(--card);border:1px solid var(--line);border-radius:9px;padding:12px 14px}
|
|
146
|
+
.kpi b{display:block;font-size:22px}
|
|
147
|
+
.kpi span{font-size:12px;color:var(--sub)}
|
|
148
|
+
.pass b{color:var(--ok)}.fail b{color:var(--bad)}
|
|
149
|
+
.bar{display:flex;height:12px;border-radius:6px;overflow:hidden;margin:6px 0 2px}
|
|
150
|
+
.bar i{display:block;height:100%}
|
|
151
|
+
.lg{font-size:11.5px;color:var(--sub)}
|
|
152
|
+
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);
|
|
153
|
+
border-radius:9px;overflow:hidden;font-size:13px;margin:10px 0 4px}
|
|
154
|
+
th,td{padding:7px 10px;border-bottom:1px solid var(--line);text-align:left}
|
|
155
|
+
th{background:#eef2f8;font-size:12px}
|
|
156
|
+
h2{font-size:17px;border-left:4px solid var(--key);padding-left:9px;margin:30px 0 10px}
|
|
157
|
+
.case{background:var(--card);border:1px solid var(--line);border-left-width:4px;border-radius:9px;
|
|
158
|
+
padding:13px 16px;margin-bottom:12px}
|
|
159
|
+
.case.pass{border-left-color:var(--ok)}.case.fail{border-left-color:var(--bad)}
|
|
160
|
+
.hd{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}
|
|
161
|
+
.hd b{font-size:15px}
|
|
162
|
+
.badge{font-size:11px;border-radius:4px;padding:1px 7px;color:#fff}
|
|
163
|
+
.b-pass{background:var(--ok)}.b-fail{background:var(--bad)}.b-l0{background:#b42318}
|
|
164
|
+
.b-l1{background:#c27803}.b-l2{background:#68707e}.b-dur{background:#3b6fd4}
|
|
165
|
+
.detail{background:#f8fafc;border-left:3px solid #cbd5e1;padding:8px 10px;font-size:12px;white-space:pre-wrap;color:#475569;margin:6px 0;max-height:220px;overflow:auto}
|
|
166
|
+
.steps{margin:8px 0 0;padding-left:0;list-style:none;font-size:13px}
|
|
167
|
+
.steps li{padding:2px 0 2px 18px;position:relative;color:#333}
|
|
168
|
+
.steps li:before{content:"▸";position:absolute;left:2px;color:#9aa3b2}
|
|
169
|
+
.steps code{background:#eef1f6;border-radius:3px;padding:0 5px;font-size:12px}
|
|
170
|
+
.reason{margin-top:8px;background:#fdf3f3;border:1px solid #f3c9c9;border-radius:7px;
|
|
171
|
+
padding:8px 11px;font:12px/1.6 ui-monospace,Menlo,monospace;color:#a31111;
|
|
172
|
+
white-space:pre-wrap;word-break:break-all}
|
|
173
|
+
.shots{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin-top:10px}
|
|
174
|
+
.shots figure{margin:0;border:1px solid var(--line);border-radius:8px;padding:7px;text-align:center;background:#fafbfd}
|
|
175
|
+
.shots img{max-width:100%;max-height:330px;border-radius:5px;border:1px solid var(--line)}
|
|
176
|
+
.shots figcaption{font-size:11.5px;color:var(--sub);margin-top:4px}
|
|
177
|
+
.fs{font-size:12px;color:var(--sub);margin-top:4px}
|
|
178
|
+
.filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0}
|
|
179
|
+
.filters button{border:1px solid var(--line);background:#fff;border-radius:14px;
|
|
180
|
+
padding:3px 13px;font-size:12.5px;cursor:pointer}
|
|
181
|
+
.filters button.on{background:var(--key);color:#fff;border-color:var(--key)}
|
|
182
|
+
.filters input{border:1px solid var(--line);border-radius:7px;padding:4px 9px;font-size:12.5px;width:170px}
|
|
183
|
+
tr.hide{display:none}
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
H = [f'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">',
|
|
187
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
188
|
+
'<title>测试执行报告</title>', f'<style>{css}</style></head><body><div class="wrap">',
|
|
189
|
+
f'<h1>测试执行报告 — {os.path.basename(os.getcwd())}</h1>',
|
|
190
|
+
f'<div class="meta">工程用例目录:{os.getcwd()} | 执行时间:{datetime.now():%Y-%m-%d %H:%M} | '
|
|
191
|
+
f'截图轮次:{os.path.relpath(run_dir) if run_dir else "无"}</div>']
|
|
192
|
+
|
|
193
|
+
# 概况
|
|
194
|
+
rate = f'{p}/{n}({p*100//n if n else 0}%)'
|
|
195
|
+
ok_pct = int(p * 100 / n) if n else 0
|
|
196
|
+
H += ['<div class="kpis">',
|
|
197
|
+
f'<div class="kpi pass"><b>{p}/{n}</b><span>通过</span></div>',
|
|
198
|
+
f'<div class="kpi fail"><b>{fails}</b><span>失败</span></div>',
|
|
199
|
+
f'<div class="kpi"><b>{rate}</b><span>通过率</span></div>',
|
|
200
|
+
f'<div class="kpi"><b>{total_dur:.0f}s</b><span>总耗时(单例均值 {total_dur/n if n else 0:.1f}s / 最长 {max(durs) if durs else 0:.0f}s)</span></div>',
|
|
201
|
+
f'<div class="kpi"><b>{len(cov)}/{len(scenarios) or "—"}</b><span>场景覆盖(引用数)</span></div>',
|
|
202
|
+
'</div>',
|
|
203
|
+
'<div class="bar">',
|
|
204
|
+
f'<i style="width:{ok_pct}%;background:var(--ok)"></i><i style="width:{100-ok_pct}%;background:var(--bad)"></i></div>',
|
|
205
|
+
f'<div class="lg">通过 {p} | 失败 {fails} | 优先级:'
|
|
206
|
+
+ ' / '.join(f'{k} {v[1]}/{v[0]}' for k, v in sorted(pr_stat.items()))
|
|
207
|
+
+ '</div>']
|
|
208
|
+
|
|
209
|
+
# 结果总览:可筛选表格
|
|
210
|
+
H += ['<h2>结果总览</h2>',
|
|
211
|
+
'<div class="filters">',
|
|
212
|
+
'<span style="font-size:12.5px;color:var(--sub)">状态:</span>',
|
|
213
|
+
'<button class="on" data-f="st" data-v="">全部</button>',
|
|
214
|
+
'<button data-f="st" data-v="PASS">PASS</button>',
|
|
215
|
+
'<button data-f="st" data-v="FAIL">FAIL</button>',
|
|
216
|
+
'<span style="font-size:12.5px;color:var(--sub)">优先级:</span>',
|
|
217
|
+
'<button class="on" data-f="pr" data-v="">全部</button>']
|
|
218
|
+
for pr in sorted(pr_stat):
|
|
219
|
+
H.append(f'<button data-f="pr" data-v="{pr}">{pr}</button>')
|
|
220
|
+
H += ['<input id="q" placeholder="搜索用例号/标题/失败原因…">',
|
|
221
|
+
'</div>',
|
|
222
|
+
'<table id="rt"><tr><th>用例</th><th>标题</th><th>结果</th><th>优先级</th>'
|
|
223
|
+
'<th>耗时</th><th>失败原因</th></tr>']
|
|
224
|
+
for cid, r in norm:
|
|
225
|
+
c = cases.get(cid, {})
|
|
226
|
+
st = r.get('status', '?')
|
|
227
|
+
stc = 'var(--ok)' if st == 'PASS' else 'var(--bad)'
|
|
228
|
+
pr = c.get('priority', '未分级')
|
|
229
|
+
msg = html.escape(str(r.get('msg', ''))[:120])
|
|
230
|
+
title = html.escape(str(c.get('title', '')))
|
|
231
|
+
H.append(f'<tr class="row" data-st="{st}" data-pr="{pr}" '
|
|
232
|
+
f'data-kw="{html.escape((cid + " " + c.get("title","") + " " + str(r.get("msg",""))).lower()[:200])}">'
|
|
233
|
+
f'<td><b>{cid}</b></td><td>{title}</td>'
|
|
234
|
+
f'<td style="color:{stc};font-weight:600">{st}</td><td>{pr}</td>'
|
|
235
|
+
f'<td>{r.get("dur","?")}s</td><td style="font-size:12px;max-width:340px">'
|
|
236
|
+
f'<span style="word-break:break-all">{msg or "—"}</span></td></tr>')
|
|
237
|
+
H += ['</table>', __JS__]
|
|
238
|
+
|
|
239
|
+
# 优先级表
|
|
240
|
+
H += ['<h2>优先级维度</h2>', '<table><tr><th>优先级</th><th>通过/总数</th><th>说明</th></tr>']
|
|
241
|
+
desc = {'L0': '冒烟/阻断——失败即阻断发版', 'L1': '重要功能——失败当版修复', 'L2': '次要/边界——择期处理', '未分级': '未标注优先级'}
|
|
242
|
+
for k, v in sorted(pr_stat.items()):
|
|
243
|
+
color = 'color:var(--ok)' if v[1] == v[0] else 'color:var(--bad)'
|
|
244
|
+
H.append(f'<tr><td><b>{k}</b></td><td style="{color}">{v[1]}/{v[0]}</td><td>{desc.get(k,"")}</td></tr>')
|
|
245
|
+
H += ['</table>']
|
|
246
|
+
|
|
247
|
+
# 用例明细
|
|
248
|
+
H += ['<h2>用例明细</h2>']
|
|
249
|
+
for cid, r in norm:
|
|
250
|
+
c = cases.get(cid, {})
|
|
251
|
+
st = r.get('status', '?')
|
|
252
|
+
cls = 'pass' if st == 'PASS' else 'fail'
|
|
253
|
+
pr = c.get('priority', '')
|
|
254
|
+
prb = f'<span class="badge b-{pr.lower() if pr else "l2"}">{pr}</span>' if pr else ''
|
|
255
|
+
fs_ids = c.get('scenarios', [])
|
|
256
|
+
fs_txt = '、'.join(f'{i}({scenarios[i]["name"]})' for i in fs_ids if i in scenarios) or '—'
|
|
257
|
+
H += [f'<div class="case {cls}">',
|
|
258
|
+
'<div class="hd">',
|
|
259
|
+
f'<span class="badge b-{"pass" if st=="PASS" else "fail"}">{st}</span>',
|
|
260
|
+
f'<b>{cid}</b>{prb}',
|
|
261
|
+
f'<span>{html.escape(str(c.get("title", "")))}</span>',
|
|
262
|
+
f'<span class="badge b-dur">{r.get("dur", "?")}s</span>',
|
|
263
|
+
'</div>',
|
|
264
|
+
f'<div class="fs">场景:{html.escape(fs_txt)}</div>']
|
|
265
|
+
steps = c.get('steps', [])
|
|
266
|
+
if steps:
|
|
267
|
+
H.append('<ul class="steps">')
|
|
268
|
+
for s_ in steps:
|
|
269
|
+
exp = {'exist': ' → 断言存在', 'absent': ' → 断言不存在'}.get(s_.get('expect'), '')
|
|
270
|
+
H.append(f'<li>{html.escape(str(s_.get("action","")))} '
|
|
271
|
+
f'<code>{html.escape(str(s_.get("target","")))}</code>{exp}</li>')
|
|
272
|
+
H.append('</ul>')
|
|
273
|
+
elif c.get('detail'): # 无设计稿(直写 py):docstring 行为链作为用例详情
|
|
274
|
+
H.append('<pre class="detail">')
|
|
275
|
+
H.append(html.escape(str(c['detail'])))
|
|
276
|
+
H.append('</pre>')
|
|
277
|
+
if r.get('msg'):
|
|
278
|
+
H.append(f'<div class="reason">失败原因:{html.escape(str(r["msg"])[:600])}</div>')
|
|
279
|
+
# 分步画廊:该用例全部步骤图(<case>_NN_label.jpeg)+ 结束/失败图
|
|
280
|
+
imgs = []
|
|
281
|
+
if run_dir:
|
|
282
|
+
pats = [os.path.join(run_dir, r.get('case', cid) + '_*.jpeg'),
|
|
283
|
+
os.path.join(run_dir, cid + '_*.jpeg'),
|
|
284
|
+
os.path.join(run_dir, cid + '_p*.jpeg')]
|
|
285
|
+
seen = set()
|
|
286
|
+
for pat in pats:
|
|
287
|
+
for g in sorted(glob.glob(pat)):
|
|
288
|
+
base = os.path.basename(g)
|
|
289
|
+
if base.endswith('_fail.jpeg') or base.endswith('_end.jpeg'):
|
|
290
|
+
continue
|
|
291
|
+
if base not in seen:
|
|
292
|
+
seen.add(base)
|
|
293
|
+
lbl = base.split('_', 2)[-1].rsplit('.', 1)[0]
|
|
294
|
+
imgs.append((os.path.relpath(g), '步骤:' + lbl))
|
|
295
|
+
s_fail = shot(cid, r.get('case', ''), '_fail')
|
|
296
|
+
if s_fail:
|
|
297
|
+
imgs.append((s_fail, '失败现场'))
|
|
298
|
+
s_end = shot(cid, r.get('case', ''), '_end')
|
|
299
|
+
if st == 'PASS' and s_end:
|
|
300
|
+
imgs.append((s_end, '用例结束页'))
|
|
301
|
+
if imgs:
|
|
302
|
+
H.append('<div class="shots">')
|
|
303
|
+
for i, (path, cap) in enumerate(imgs):
|
|
304
|
+
src = path
|
|
305
|
+
if EMBED: # 关键帧内嵌:fail/end 必嵌 + 过程帧首末张;其余保留相对路径(同目录可看全图)
|
|
306
|
+
key = ('失败现场' in cap) or ('结束页' in cap) or i in (0, len(imgs) - 2)
|
|
307
|
+
if key:
|
|
308
|
+
b64 = thumb_b64(path)
|
|
309
|
+
if b64:
|
|
310
|
+
src = b64
|
|
311
|
+
H.append(f'<figure><img src="{src}" loading="lazy"><figcaption>{cap}</figcaption></figure>')
|
|
312
|
+
H.append('</div>')
|
|
313
|
+
H.append('</div>')
|
|
314
|
+
|
|
315
|
+
H += ['<div class="meta" style="margin-top:30px">由 forge_report.py 生成 | '
|
|
316
|
+
'数据:results.json + cases.json + scenarios.json | 截图:run_direct 执行时自动采集</div>',
|
|
317
|
+
'</div></body></html>']
|
|
318
|
+
|
|
319
|
+
with open(args.out, 'w', encoding='utf-8') as f:
|
|
320
|
+
f.write('\n'.join(H))
|
|
321
|
+
print(f'[written] {args.out}({n} 用例 / PASS {p} / 截图目录 {os.path.relpath(run_dir) if run_dir else "无"})')
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == '__main__':
|
|
325
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
inject_resource_ids.py — 资源化文案工程的 ID 注入器(DFT-010 场景)。
|
|
4
|
+
|
|
5
|
+
流程:target 文本 → string.json 资源名 → 全工程搜 $r('app.string.X') 引用点
|
|
6
|
+
→ 组件属性链内注入 .id('tc_<file>_<hash>')(幂等,已有 tc_ id 复用)
|
|
7
|
+
|
|
8
|
+
输出 injected_ids.json(target → id 映射,供脚本生成器消费)
|
|
9
|
+
用法: inject_resource_ids.py <harmony_root> <targets_json文件或逗号分隔> [--cases cases.json]
|
|
10
|
+
"""
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import hashlib
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_res_map(root):
|
|
19
|
+
res_map = {}
|
|
20
|
+
for dp, _d, files in os.walk(root):
|
|
21
|
+
if 'oh_modules' in dp or '/build' in dp or '.testdesign' in dp or '.hypium_forge' in dp:
|
|
22
|
+
continue
|
|
23
|
+
for f in files:
|
|
24
|
+
if f == 'string.json':
|
|
25
|
+
try:
|
|
26
|
+
for item in json.load(open(os.path.join(dp, f), encoding='utf-8')).get('string', []):
|
|
27
|
+
res_map.setdefault(item['value'], item['name'])
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
return res_map
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def collect_src(root):
|
|
34
|
+
out = []
|
|
35
|
+
for dp, _d, files in os.walk(root):
|
|
36
|
+
if 'oh_modules' in dp or '/build' in dp or '.testdesign' in dp or '.hypium_forge' in dp:
|
|
37
|
+
continue
|
|
38
|
+
for f in files:
|
|
39
|
+
if f.endswith('.ets'):
|
|
40
|
+
out.append(os.path.join(dp, f))
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
ID_RE = re.compile(r"\.id\('tc_[a-z0-9_]+'\)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def inject_in_file(sf, rx):
|
|
48
|
+
"""在 sf 中找 rx 引用行,属性链内注入 id;返回 (tid or None)。幂等。"""
|
|
49
|
+
src = open(sf, encoding='utf-8', errors='ignore').read()
|
|
50
|
+
lines = src.split('\n')
|
|
51
|
+
for i, ln in enumerate(lines):
|
|
52
|
+
if rx.search(ln):
|
|
53
|
+
seg = '\n'.join(lines[i:i + 12])
|
|
54
|
+
m = ID_RE.search(seg)
|
|
55
|
+
if m:
|
|
56
|
+
return m.group(0)[5:-2] # 已有 → 复用
|
|
57
|
+
for k in range(i, min(len(lines), i + 10)):
|
|
58
|
+
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
59
|
+
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
60
|
+
tid = 'tc_' + os.path.splitext(os.path.basename(sf))[0].lower() + '_' + \
|
|
61
|
+
hashlib.md5(ln.encode()).hexdigest()[:6]
|
|
62
|
+
lines.insert(k, f"{indent}.id('{tid}')")
|
|
63
|
+
open(sf, 'w', encoding='utf-8').write('\n'.join(lines))
|
|
64
|
+
return tid
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
ap = argparse.ArgumentParser()
|
|
70
|
+
ap.add_argument('root')
|
|
71
|
+
ap.add_argument('--cases', help='forge 格式 cases.json(取 target)')
|
|
72
|
+
ap.add_argument('--targets', help='逗号分隔 target 列表(二选一)')
|
|
73
|
+
ap.add_argument('--out', default=None)
|
|
74
|
+
args = ap.parse_args()
|
|
75
|
+
|
|
76
|
+
targets = []
|
|
77
|
+
if args.cases:
|
|
78
|
+
for c in json.load(open(args.cases, encoding='utf-8')):
|
|
79
|
+
for s in c.get('steps', []):
|
|
80
|
+
t = s.get('target')
|
|
81
|
+
if t and not t.startswith('请输入') and t != '数量':
|
|
82
|
+
targets.append(t)
|
|
83
|
+
else:
|
|
84
|
+
targets = args.targets.split(',')
|
|
85
|
+
targets = list(dict.fromkeys(targets))
|
|
86
|
+
|
|
87
|
+
res_map = build_res_map(args.root)
|
|
88
|
+
src_files = collect_src(args.root)
|
|
89
|
+
|
|
90
|
+
injected, unresolved = {}, []
|
|
91
|
+
for t in targets:
|
|
92
|
+
resname = res_map.get(t)
|
|
93
|
+
tid = None
|
|
94
|
+
if resname:
|
|
95
|
+
rx = re.compile(r"\$r\(\s*['\"]app\.string\." + re.escape(resname) + r"['\"]")
|
|
96
|
+
for sf in src_files:
|
|
97
|
+
tid = inject_in_file(sf, rx)
|
|
98
|
+
if tid:
|
|
99
|
+
injected[t] = tid
|
|
100
|
+
break
|
|
101
|
+
if not tid:
|
|
102
|
+
unresolved.append(t)
|
|
103
|
+
|
|
104
|
+
out = args.out or os.path.join(args.root, '.hypium_forge', 'injected_ids.json')
|
|
105
|
+
json.dump(injected, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
|
106
|
+
print(f'资源命中注入: {len(injected)} / 未解析: {len(unresolved)} → {out}')
|
|
107
|
+
for t, tid in sorted(injected.items()):
|
|
108
|
+
print(f' + {t} → {tid}')
|
|
109
|
+
if unresolved:
|
|
110
|
+
print('未解析(动态文案/图标按钮,脚本走文本/兜底定位):', unresolved[:15])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == '__main__':
|
|
114
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lint_asserts.py — 断言强度门禁(白盒直写形态,2026-09-29 none 臂对照实证落地)。
|
|
3
|
+
|
|
4
|
+
背景:none 臂(无 skill)LLM 凭通用能力达成 34% 强断言(hilog 防污染/dump Toast 轮询/
|
|
5
|
+
剪贴板),B 线(抄模板)仅 15%——模板能力边界效应。本门禁把下限固化:
|
|
6
|
+
- 强断言步骤占比 ≥ 30%(默认阈值),低于即 FAIL
|
|
7
|
+
- vacuous 用例点名(纯点击链、无任何值级断言)——对应 assert_audit.py 语义(.py 版)
|
|
8
|
+
|
|
9
|
+
值级断言原语:checked/unchecked/enabled/equals/text_equals/text_absent/toast_text_seen/
|
|
10
|
+
hilog_tail(关键字)/pasteboard/剪贴板/_dump_layout 断言性使用/text_contains/count
|
|
11
|
+
用法: python3 lint_asserts.py --dir testcases [--threshold 30]
|
|
12
|
+
"""
|
|
13
|
+
import argparse
|
|
14
|
+
import glob
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
STRONG_CALLS = re.compile(
|
|
20
|
+
r'\b(checked|unchecked|enabled\(|disabled|text_equals|text_absent|toast_text_seen|'
|
|
21
|
+
r'hilog_clear|hilog_tail|pasteboard|剪贴板|text_contains|find_all_components\s*\(\s*BY\.)'
|
|
22
|
+
r'[^)]*\)|\bchecked\s*\(\s*True\)|assert\s')
|
|
23
|
+
STEP_PAT = re.compile(r'^\s*(?:Step\(|step_shot\(|#)', re.M)
|
|
24
|
+
ACTION_PAT = re.compile(r'\b(id_click|text_click|click_text|click_tab|id_input|d\.touch|press_back|swipe|scroll_click)\b')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
STRONG_STEP_WORDS = ('断言', '校验', 'equals', 'checked', 'absent', 'toast', 'hilog',
|
|
28
|
+
'剪贴板', 'pasteboard', '包含', '等于', '消失', '翻转为', '已开启', '已关闭',
|
|
29
|
+
'一致性', '业务数据', '关键字', 'dump')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
VALUE_CALLS = re.compile(
|
|
33
|
+
r'\b(text_equals|text_absent|toast_text_seen|hilog_clear|hilog_tail|pasteboard|'
|
|
34
|
+
r'find_all_components|checked\s*\(|asserts?\s*\.|\.assert)')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def audit_file(path):
|
|
38
|
+
"""双轨审计(两种白盒写法都覆盖):
|
|
39
|
+
轨1 Step 结构:Step 描述含值级断言语义词 = 强步骤;
|
|
40
|
+
轨2 无 Step(内联代码):值级断言调用数 / (值级调用+动作调用) 为强度。"""
|
|
41
|
+
src = open(path, encoding='utf-8').read()
|
|
42
|
+
steps_all = re.findall(r'Step\(\s*"([^"]+)"', src)
|
|
43
|
+
value_calls = len(VALUE_CALLS.findall(src))
|
|
44
|
+
if steps_all:
|
|
45
|
+
strong = sum(1 for t in steps_all if any(w in t.lower() for w in STRONG_STEP_WORDS))
|
|
46
|
+
return len(steps_all), 0, strong, strong > 0 or value_calls > 0
|
|
47
|
+
actions = len(ACTION_PAT.findall(src))
|
|
48
|
+
return value_calls + actions, 0, value_calls, value_calls > 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
ap = argparse.ArgumentParser()
|
|
53
|
+
ap.add_argument('--dir', required=True)
|
|
54
|
+
ap.add_argument('--threshold', type=int, default=30)
|
|
55
|
+
args = ap.parse_args()
|
|
56
|
+
total_steps = total_strong = vacuous = n = 0
|
|
57
|
+
vacuous_list = []
|
|
58
|
+
for p in sorted(glob.glob(os.path.join(args.dir, 'TC*.py'))):
|
|
59
|
+
steps, marks, strong_steps, has_value = audit_file(p)
|
|
60
|
+
n += 1
|
|
61
|
+
total_steps += steps
|
|
62
|
+
total_strong += (strong_steps or marks)
|
|
63
|
+
if not has_value:
|
|
64
|
+
vacuous += 1
|
|
65
|
+
vacuous_list.append(os.path.basename(p))
|
|
66
|
+
pct = round(total_strong / total_steps * 100, 1) if total_steps else 0
|
|
67
|
+
print(f'断言强度: 强断言 {total_strong}/{total_steps} 步 = {pct}%(下限 {args.threshold}%)')
|
|
68
|
+
if vacuous_list:
|
|
69
|
+
print(f'⚠ vacuous 用例(无任何值级断言){vacuous} 个: {", ".join(vacuous_list[:8])}')
|
|
70
|
+
if n and pct < args.threshold:
|
|
71
|
+
print(f'✗ FAIL:断言强度 {pct}% < {args.threshold}%——按场景目的补值级断言(最佳区间 40~70%)'
|
|
72
|
+
f'(Toast→toast_text_seen;系统事件→hilog 轮询+用例前 hilog_clear;'
|
|
73
|
+
f'状态→checked;文案→text_equals;消失→text_absent)后重检')
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
print('✓ PASS')
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == '__main__':
|
|
80
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lint_locate.py — 脚本定位方式自检门禁(铁律①的执行期强制)。
|
|
3
|
+
|
|
4
|
+
统计 testcases/*.py 的定位调用(id_click 等 BY.key 原语 vs text_click/click_text),
|
|
5
|
+
text 定位占比超阈值(默认 20%)即 FAIL 并列出改写建议(ids.json 有映射优先)。
|
|
6
|
+
写完脚本后必须先过本门禁再执行。
|
|
7
|
+
|
|
8
|
+
用法: python3 lint_locate.py --dir testcases [--ids ids.json] [--threshold 20]
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import glob
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
KEY_PRIMS = r'\bid_click\(|BY\.key\('
|
|
18
|
+
TEXT_PRIMS = r'\btext_click\(|\bclick_text\(|BY\.text\((?![^)]*CONTAINS)|find_component\(\s*BY\.text'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
ap = argparse.ArgumentParser()
|
|
23
|
+
ap.add_argument('--dir', required=True)
|
|
24
|
+
ap.add_argument('--ids', default=None)
|
|
25
|
+
ap.add_argument('--threshold', type=int, default=20)
|
|
26
|
+
args = ap.parse_args()
|
|
27
|
+
ids = {}
|
|
28
|
+
if args.ids and os.path.exists(args.ids):
|
|
29
|
+
ids = json.load(open(args.ids, encoding='utf-8'))
|
|
30
|
+
total_key = total_text = 0
|
|
31
|
+
offenders = []
|
|
32
|
+
for p in sorted(glob.glob(os.path.join(args.dir, 'TC*.py'))):
|
|
33
|
+
src = open(p, encoding='utf-8').read()
|
|
34
|
+
k = len(re.findall(KEY_PRIMS, src))
|
|
35
|
+
t = len(re.findall(TEXT_PRIMS, src))
|
|
36
|
+
total_key += k
|
|
37
|
+
total_text += t
|
|
38
|
+
if t > k:
|
|
39
|
+
hint = ''
|
|
40
|
+
m = re.findall(r'(?:text_click|click_text)\(\s*d\s*,\s*[\'"]([^\'"]+)[\'"]', src)
|
|
41
|
+
mapped = [f"{x} → {ids[x]}" for x in m if x in ids][:3]
|
|
42
|
+
if mapped:
|
|
43
|
+
hint = ' | ids.json 有映射: ' + '; '.join(mapped)
|
|
44
|
+
offenders.append(f"{os.path.basename(p)}: key={k} text={t}{hint}")
|
|
45
|
+
total = total_key + total_text
|
|
46
|
+
pct = round(total_key / total * 100) if total else 100
|
|
47
|
+
print(f"定位统计: BY.key={total_key} / text={total_text} → key 占比 {pct}%(阈值 ≥{100 - args.threshold}%)")
|
|
48
|
+
if offenders:
|
|
49
|
+
print(f"text 主导的脚本 {len(offenders)} 个:")
|
|
50
|
+
for o in offenders[:15]:
|
|
51
|
+
print(' -', o)
|
|
52
|
+
if total and pct < 100 - args.threshold:
|
|
53
|
+
print(f"✗ FAIL:text 定位占比 {100 - pct}% > {args.threshold}% —— 须把 ids.json 有映射的 text_click 改写为 id_click 后重过门禁")
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
print('✓ PASS')
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
main()
|