@harmonyos-arkts/d2h 0.1.10 → 0.1.12
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/.claude-plugin/marketplace.json +15 -13
- package/.claude-plugin/plugin.json +13 -7
- package/AGENTS.md +5 -5
- package/README.md +212 -212
- package/agents/android-to-hmos-00-orchestrator.md +318 -318
- package/agents/d2h.md +157 -157
- package/dist/bin/install-opencode.js +30 -30
- package/docs/environment-check.md +86 -86
- package/install-opencode.sh +160 -160
- package/opencode/agents.json +16 -16
- package/opencode/vscode-d2h-package.json +7 -7
- package/opencode/vscode-d2h-plugin.mjs +29 -29
- package/package.json +54 -54
- package/schemas/android-source-manifest.schema.json +25 -25
- package/schemas/checkpoint-provenance.schema.json +67 -67
- package/schemas/final-acceptance.schema.json +29 -29
- package/schemas/managed-evidence-index.schema.json +15 -15
- package/schemas/managed-evidence.schema.json +114 -114
- package/schemas/migration-config.schema.json +51 -51
- package/schemas/migration-report-index.schema.json +39 -39
- package/schemas/migration-report-item.schema.json +51 -51
- package/schemas/migration-report-summary.schema.json +25 -25
- package/schemas/migration-status.schema.json +202 -202
- package/schemas/preflight.schema.json +60 -60
- package/schemas/source-order-audit.schema.json +16 -16
- package/schemas/source-provenance-event.schema.json +19 -19
- package/schemas/spec-app-shard.schema.json +45 -45
- package/schemas/spec-fact-corrections-shard.schema.json +13 -13
- package/schemas/spec-features-shard.schema.json +43 -43
- package/schemas/spec-index.schema.json +26 -26
- package/schemas/spec-interactions-shard.schema.json +40 -40
- package/schemas/spec-page.schema.json +98 -98
- package/schemas/spec-pages-index.schema.json +1 -1
- package/schemas/spec-unresolved-shard.schema.json +1 -1
- package/schemas/task-envelope.schema.json +109 -109
- package/schemas/task-plan.schema.json +341 -341
- package/scripts/check-environment.mjs +320 -320
- package/skills/android2hmos_resources_convert/SKILL.md +180 -180
- package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
- package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
- package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
- package/skills/arkts-app-identity/SKILL.md +238 -238
- package/skills/arkts-i18n/SKILL.md +496 -496
- package/skills/arkts-i18n/evals/evals.json +84 -84
- package/skills/arkts-i18n/references/code-examples.md +302 -302
- package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
- package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
- package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
- package/skills/arkts-i18n/references/language-codes.md +104 -104
- package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
- package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
- package/skills/arkts-i18n/references/v1-compat.md +244 -244
- package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
- package/skills/arkts-icon-sizing/SKILL.md +211 -211
- package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
- package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
- package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
- package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
- package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
- package/skills/d2h-hypium-forge/SKILL.md +309 -309
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
- package/skills/d2h-hypium-forge/references/official-api.md +135 -135
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
- package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
- package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
- package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
- package/skills/d2h-page-develop/SKILL.md +112 -112
- package/skills/d2h-page-develop/agents/openai.yaml +7 -7
- package/skills/d2h-page-spec-generate/SKILL.md +206 -206
- package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
- package/skills/d2h-utg-static-generate/SKILL.md +73 -73
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
- package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
- package/skills/maintaining-migration-report/SKILL.md +144 -144
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
- package/skills/managing-d2h-toolchain/SKILL.md +24 -24
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
- package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
- package/skills/managing-d2h-toolchain/toolchain.json +16 -16
- package/skills/native-library-substitution/SKILL.md +384 -384
- package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
- package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
- package/skills/preparing-migration-workspace/SKILL.md +131 -131
- package/skills/reviewing-migration-process/SKILL.md +62 -62
|
@@ -1,255 +1,255 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
scan_features.py — 扫描鸿蒙工程,产出功能场景矩阵(features.json + features.md)。
|
|
4
|
-
|
|
5
|
-
场景矩阵模型(覆盖度评估的基础):
|
|
6
|
-
页面(路由可达) × 交互点(可操作控件/事件) × 状态(数据/条件分支)
|
|
7
|
-
|
|
8
|
-
输出 features.json:
|
|
9
|
-
{
|
|
10
|
-
"project_dir": "...",
|
|
11
|
-
"bundle": "hm.xxx",
|
|
12
|
-
"pages": [
|
|
13
|
-
{
|
|
14
|
-
"page": "Index", "path": "pages/Index", "file": "...",
|
|
15
|
-
"nav_in": ["入口"] , # 如何到达(入口/router.pushUrl 链)
|
|
16
|
-
"interactions": [ # 交互点(含已有 ID)
|
|
17
|
-
{"kind": "click", "target": "id_or_text", "desc": "设置按钮", "has_id": true}
|
|
18
|
-
],
|
|
19
|
-
"states": [ # 可区分状态(条件渲染/数据驱动)
|
|
20
|
-
{"name": "列表为空", "evidence": "if (!items.length)", "line": 42}
|
|
21
|
-
],
|
|
22
|
-
"data_sources": ["mock", "http", "preferences"]
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"permissions": ["ohos.permission.INTERNET", ...], # 需预授/确认的权限
|
|
26
|
-
"has_login": true|false, # 登录页检测
|
|
27
|
-
"nav_graph": {"pages/Index": ["pages/Settings", ...]} # 静态跳转图
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
用法: scan_features.py <harmony_project_dir> [--out features.json]
|
|
31
|
-
"""
|
|
32
|
-
import argparse
|
|
33
|
-
import json
|
|
34
|
-
import os
|
|
35
|
-
import re
|
|
36
|
-
import sys
|
|
37
|
-
|
|
38
|
-
# 登录页特征
|
|
39
|
-
LOGIN_HINTS = ['登录', 'login', 'Login', 'signin', 'SignIn', '账号', '密码', 'password', '手机号']
|
|
40
|
-
# 数据源特征
|
|
41
|
-
DATA_HINTS = {
|
|
42
|
-
'http': re.compile(r'https?://|@ohos\.net\.http|axios|rcp'),
|
|
43
|
-
'preferences': re.compile(r'@ohos\.data\.preferences|preferences\.getPreferences'),
|
|
44
|
-
'mock': re.compile(r'\bmock\b|MockData|EMOJI_DATA|SAMPLE_', re.I),
|
|
45
|
-
'db': re.compile(r'@ohos\.data\.relationalStore|relationalStore'),
|
|
46
|
-
}
|
|
47
|
-
# 交互控件(组件级)
|
|
48
|
-
INTERACTIVE_COMPONENTS = ('Button', 'TextInput', 'TextArea', 'Toggle', 'Slider',
|
|
49
|
-
'Radio', 'Checkbox', 'Select', 'Search', 'Stepper',
|
|
50
|
-
'Rating', 'DatePicker', 'TextPicker', 'TimePicker',
|
|
51
|
-
'Image', 'Hyperlink', 'Navigator')
|
|
52
|
-
# 非组件关键词(生命周期/builder/容器等误判源)
|
|
53
|
-
NON_COMPONENT = re.compile(r'^(onPageShow|onBackPress|aboutToAppear|aboutToDisappear|'
|
|
54
|
-
r'\w*[Bb]uilder|TabContent|ForEach|LazyForEach|Repeat|'
|
|
55
|
-
r'Navigation|NavDestination|if|else|while|for|struct|class|build)\b')
|
|
56
|
-
# 风险权限(需用户确认/无法预授)
|
|
57
|
-
RISKY_PERMS = {'ohos.permission.USER_AUTH', 'ohos.permission.MANAGE_FINGERPRINT'}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def read(p):
|
|
61
|
-
try:
|
|
62
|
-
return open(p, encoding='utf-8', errors='ignore').read()
|
|
63
|
-
except OSError:
|
|
64
|
-
return ''
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def find_pages(root):
|
|
68
|
-
"""定位入口模块与页面集。"""
|
|
69
|
-
# 找含 UIAbility+loadContent 的模块(entry 或自定义)
|
|
70
|
-
modules = []
|
|
71
|
-
for cand in sorted(os.listdir(root)):
|
|
72
|
-
mdir = os.path.join(root, cand)
|
|
73
|
-
if os.path.isdir(mdir) and os.path.isdir(os.path.join(mdir, 'src', 'main')):
|
|
74
|
-
modules.append(cand)
|
|
75
|
-
elif os.path.isdir(mdir) and cand not in ('oh_modules', '.hypium_forge', 'AppScope', 'hvigor', 'build', 'doc'):
|
|
76
|
-
# 二级模块容器(features/commons 下可能有独立模块)
|
|
77
|
-
for sub in sorted(os.listdir(mdir)):
|
|
78
|
-
sdir = os.path.join(mdir, sub)
|
|
79
|
-
if os.path.isdir(os.path.join(sdir, 'src', 'main')):
|
|
80
|
-
modules.append(f'{cand}/{sub}')
|
|
81
|
-
|
|
82
|
-
pages = {}
|
|
83
|
-
nav_graph = {}
|
|
84
|
-
entry_module, entry_page = None, None
|
|
85
|
-
for m in modules:
|
|
86
|
-
mdir = os.path.join(root, m, 'src', 'main')
|
|
87
|
-
# Ability 的 loadContent
|
|
88
|
-
for dp, _d, files in os.walk(os.path.join(mdir, 'ets')):
|
|
89
|
-
for f in files:
|
|
90
|
-
if f.endswith('.ets'):
|
|
91
|
-
src = read(os.path.join(dp, f))
|
|
92
|
-
mm = re.search(r"loadContent\(\s*'([^']+)'", src)
|
|
93
|
-
if mm and entry_page is None:
|
|
94
|
-
entry_module, entry_page = m, mm.group(1)
|
|
95
|
-
# 路由表(可能多个)
|
|
96
|
-
for dp, _d, files in os.walk(os.path.join(mdir, 'resources')):
|
|
97
|
-
for f in files:
|
|
98
|
-
if f.endswith('_pages.json'):
|
|
99
|
-
try:
|
|
100
|
-
routes = json.load(open(os.path.join(dp, f))).get('src', [])
|
|
101
|
-
except (OSError, json.JSONDecodeError):
|
|
102
|
-
routes = []
|
|
103
|
-
for r in routes:
|
|
104
|
-
pages.setdefault((m, r), None)
|
|
105
|
-
# @Entry 页面兜底(覆盖路由表留下的 None 值)
|
|
106
|
-
ets_root = os.path.join(mdir, 'ets')
|
|
107
|
-
for dp, _d, files in os.walk(ets_root):
|
|
108
|
-
for f in files:
|
|
109
|
-
if f.endswith('.ets') and '@Entry' in read(os.path.join(dp, f)):
|
|
110
|
-
rel = os.path.relpath(os.path.join(dp, f), ets_root)[:-4]
|
|
111
|
-
if pages.get((m, rel)) is None:
|
|
112
|
-
pages[(m, rel)] = os.path.join(dp, f)
|
|
113
|
-
return modules, pages, entry_module, entry_page
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def scan_page(file_path):
|
|
117
|
-
"""单页扫描:交互点/状态/数据源/跳转。"""
|
|
118
|
-
src = read(file_path)
|
|
119
|
-
lines = src.split('\n')
|
|
120
|
-
interactions, states, navs = [], [], []
|
|
121
|
-
data_sources = [k for k, rx in DATA_HINTS.items() if rx.search(src)]
|
|
122
|
-
|
|
123
|
-
# 组件构造 + 邻近 .id()/文本
|
|
124
|
-
for i, ln in enumerate(lines):
|
|
125
|
-
m = re.match(r'^\s*(\w+)\s*(?:\(\s*(?:{[^}]*})?\s*\)?\s*(?:\{\s*)?)$', ln) or \
|
|
126
|
-
re.match(r'^\s*(Button|Text|Image|Toggle|TextInput)\s*\(', ln)
|
|
127
|
-
if not m or NON_COMPONENT.match(m.group(1)):
|
|
128
|
-
continue
|
|
129
|
-
comp = m.group(1)
|
|
130
|
-
# 向后 15 行找 .id / 文本内容 / onClick
|
|
131
|
-
after = '\n'.join(lines[i:min(len(lines), i + 15)])
|
|
132
|
-
idm = re.search(r"\.id\(\s*'([^']+)'", after)
|
|
133
|
-
txtm = re.search(r"(?:Text|Button)\s*\(\s*(?:this\.)?[\w.]*?'([^']{1,24})'", '\n'.join(lines[max(0,i-1):i+6]))
|
|
134
|
-
onclick = bool(re.search(r'\.on[A-Z]\w*\(', after))
|
|
135
|
-
if comp in INTERACTIVE_COMPONENTS or onclick:
|
|
136
|
-
target = idm.group(1) if idm else (txtm.group(1) if txtm else None)
|
|
137
|
-
if comp == 'Image' and not onclick:
|
|
138
|
-
continue
|
|
139
|
-
# 纯容器按行号定位且无文本无 id —— 噪声,剔除
|
|
140
|
-
if not target and comp in ('Column', 'Row', 'Stack', 'Flex', 'ListItem', 'List', 'Grid', 'Refresh', 'TabContent'):
|
|
141
|
-
continue
|
|
142
|
-
interactions.append({
|
|
143
|
-
'kind': 'click' if comp != 'TextInput' else 'input',
|
|
144
|
-
'target': target or f'line{i+1}:{comp}',
|
|
145
|
-
'desc': (txtm.group(1) if txtm else comp),
|
|
146
|
-
'has_id': bool(idm), 'line': i + 1, 'type': comp
|
|
147
|
-
})
|
|
148
|
-
# router 跳转
|
|
149
|
-
for nm in re.finditer(r"router\.(?:pushUrl|replaceUrl)\(\s*{\s*url:\s*'([^']+)'", after):
|
|
150
|
-
navs.append(nm.group(1))
|
|
151
|
-
|
|
152
|
-
# 条件状态(if 分支)
|
|
153
|
-
for i, ln in enumerate(lines):
|
|
154
|
-
if re.match(r'^\s*if\s*\(', ln):
|
|
155
|
-
cond = ln.strip()
|
|
156
|
-
states.append({'name': cond[:60], 'evidence': cond, 'line': i + 1})
|
|
157
|
-
return interactions, states[:20], navs, data_sources
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def main():
|
|
161
|
-
ap = argparse.ArgumentParser()
|
|
162
|
-
ap.add_argument('harmony')
|
|
163
|
-
ap.add_argument('--out', default=None)
|
|
164
|
-
args = ap.parse_args()
|
|
165
|
-
root = args.harmony
|
|
166
|
-
|
|
167
|
-
app_json5 = os.path.join(root, 'AppScope', 'app.json5')
|
|
168
|
-
bundle = ''
|
|
169
|
-
m = re.search(r'"bundleName"\s*:\s*"([^"]+)"', read(app_json5))
|
|
170
|
-
if m:
|
|
171
|
-
bundle = m.group(1)
|
|
172
|
-
|
|
173
|
-
modules, pages, entry_module, entry_page = find_pages(root)
|
|
174
|
-
if not pages:
|
|
175
|
-
print('错误: 未找到任何页面(路由表或 @Entry)', file=sys.stderr)
|
|
176
|
-
sys.exit(1)
|
|
177
|
-
|
|
178
|
-
# 权限(各模块 module.json5)
|
|
179
|
-
perms = []
|
|
180
|
-
for mdir in modules:
|
|
181
|
-
mj = os.path.join(root, mdir, 'src', 'main', 'module.json5')
|
|
182
|
-
perms += re.findall(r'"name"\s*:\s*"(ohos\.permission\.[\w.]+)"', read(mj))
|
|
183
|
-
perms = sorted(set(perms))
|
|
184
|
-
|
|
185
|
-
# 登录检测
|
|
186
|
-
has_login = False
|
|
187
|
-
for (mname, route), f in pages.items():
|
|
188
|
-
src = read(f) if f else ''
|
|
189
|
-
if any(h in src for h in LOGIN_HINTS):
|
|
190
|
-
# 有登录字样的页面
|
|
191
|
-
if re.search(r'登录|login|Login', src):
|
|
192
|
-
has_login = True
|
|
193
|
-
|
|
194
|
-
result = {'project_dir': os.path.abspath(root), 'bundle': bundle,
|
|
195
|
-
'entry': {'module': entry_module, 'page': entry_page},
|
|
196
|
-
'pages': [], 'permissions': perms, 'has_login': has_login,
|
|
197
|
-
'nav_graph': {}}
|
|
198
|
-
|
|
199
|
-
n_int = n_state = 0
|
|
200
|
-
for (mname, route), f in sorted(pages.items()):
|
|
201
|
-
if not f:
|
|
202
|
-
continue
|
|
203
|
-
interactions, states, navs, data_sources = scan_page(f)
|
|
204
|
-
result['pages'].append({
|
|
205
|
-
'page': os.path.basename(route), 'path': route, 'module': mname,
|
|
206
|
-
'file': os.path.abspath(f),
|
|
207
|
-
'nav_in': ['entry'] if route == entry_page else ['router:见 nav_graph'],
|
|
208
|
-
'interactions': interactions, 'states': states,
|
|
209
|
-
'data_sources': data_sources
|
|
210
|
-
})
|
|
211
|
-
result['nav_graph'][route] = sorted(set(navs))
|
|
212
|
-
n_int += len(interactions)
|
|
213
|
-
n_state += len(states)
|
|
214
|
-
|
|
215
|
-
# ── 深扫:多模块架构的真实 UI 在 features/commons 的 @Component 组件里 ──
|
|
216
|
-
seen_files = {p['file'] for p in result['pages']}
|
|
217
|
-
for m in modules:
|
|
218
|
-
ets_root = os.path.join(root, m, 'src', 'main', 'ets')
|
|
219
|
-
if not os.path.isdir(ets_root):
|
|
220
|
-
continue
|
|
221
|
-
for dp, _d, files in os.walk(ets_root):
|
|
222
|
-
for f in files:
|
|
223
|
-
if not f.endswith('.ets'):
|
|
224
|
-
continue
|
|
225
|
-
fp = os.path.join(dp, f)
|
|
226
|
-
if fp in seen_files:
|
|
227
|
-
continue
|
|
228
|
-
src = read(fp)
|
|
229
|
-
# 含 build() 的组件(@Component/@ComponentV2 struct)才算 UI 单元
|
|
230
|
-
if '@Component' in src and re.search(r'\bbuild\s*\(\s*\)', src):
|
|
231
|
-
interactions, states, navs, data_sources = scan_page(fp)
|
|
232
|
-
if not interactions and not states:
|
|
233
|
-
continue
|
|
234
|
-
rel = os.path.relpath(fp, os.path.join(root, m, 'src', 'main', 'ets'))[:-4]
|
|
235
|
-
result['pages'].append({
|
|
236
|
-
'page': os.path.splitext(f)[0], 'path': f'comp:{m}/{rel}',
|
|
237
|
-
'module': m, 'file': os.path.abspath(fp),
|
|
238
|
-
'nav_in': ['builder/comp'], 'interactions': interactions,
|
|
239
|
-
'states': states, 'data_sources': data_sources
|
|
240
|
-
})
|
|
241
|
-
n_int += len(interactions)
|
|
242
|
-
n_state += len(states)
|
|
243
|
-
|
|
244
|
-
payload = json.dumps(result, indent=2, ensure_ascii=False)
|
|
245
|
-
if args.out:
|
|
246
|
-
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
|
|
247
|
-
open(args.out, 'w', encoding='utf-8').write(payload)
|
|
248
|
-
print(f'扫描完成: {len(result["pages"])} 页面 / {n_int} 交互点 / {n_state} 状态 / '
|
|
249
|
-
f'{len(perms)} 权限 / 登录页={"有" if has_login else "无"} → {args.out}')
|
|
250
|
-
else:
|
|
251
|
-
print(payload)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if __name__ == '__main__':
|
|
255
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
scan_features.py — 扫描鸿蒙工程,产出功能场景矩阵(features.json + features.md)。
|
|
4
|
+
|
|
5
|
+
场景矩阵模型(覆盖度评估的基础):
|
|
6
|
+
页面(路由可达) × 交互点(可操作控件/事件) × 状态(数据/条件分支)
|
|
7
|
+
|
|
8
|
+
输出 features.json:
|
|
9
|
+
{
|
|
10
|
+
"project_dir": "...",
|
|
11
|
+
"bundle": "hm.xxx",
|
|
12
|
+
"pages": [
|
|
13
|
+
{
|
|
14
|
+
"page": "Index", "path": "pages/Index", "file": "...",
|
|
15
|
+
"nav_in": ["入口"] , # 如何到达(入口/router.pushUrl 链)
|
|
16
|
+
"interactions": [ # 交互点(含已有 ID)
|
|
17
|
+
{"kind": "click", "target": "id_or_text", "desc": "设置按钮", "has_id": true}
|
|
18
|
+
],
|
|
19
|
+
"states": [ # 可区分状态(条件渲染/数据驱动)
|
|
20
|
+
{"name": "列表为空", "evidence": "if (!items.length)", "line": 42}
|
|
21
|
+
],
|
|
22
|
+
"data_sources": ["mock", "http", "preferences"]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"permissions": ["ohos.permission.INTERNET", ...], # 需预授/确认的权限
|
|
26
|
+
"has_login": true|false, # 登录页检测
|
|
27
|
+
"nav_graph": {"pages/Index": ["pages/Settings", ...]} # 静态跳转图
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
用法: scan_features.py <harmony_project_dir> [--out features.json]
|
|
31
|
+
"""
|
|
32
|
+
import argparse
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
# 登录页特征
|
|
39
|
+
LOGIN_HINTS = ['登录', 'login', 'Login', 'signin', 'SignIn', '账号', '密码', 'password', '手机号']
|
|
40
|
+
# 数据源特征
|
|
41
|
+
DATA_HINTS = {
|
|
42
|
+
'http': re.compile(r'https?://|@ohos\.net\.http|axios|rcp'),
|
|
43
|
+
'preferences': re.compile(r'@ohos\.data\.preferences|preferences\.getPreferences'),
|
|
44
|
+
'mock': re.compile(r'\bmock\b|MockData|EMOJI_DATA|SAMPLE_', re.I),
|
|
45
|
+
'db': re.compile(r'@ohos\.data\.relationalStore|relationalStore'),
|
|
46
|
+
}
|
|
47
|
+
# 交互控件(组件级)
|
|
48
|
+
INTERACTIVE_COMPONENTS = ('Button', 'TextInput', 'TextArea', 'Toggle', 'Slider',
|
|
49
|
+
'Radio', 'Checkbox', 'Select', 'Search', 'Stepper',
|
|
50
|
+
'Rating', 'DatePicker', 'TextPicker', 'TimePicker',
|
|
51
|
+
'Image', 'Hyperlink', 'Navigator')
|
|
52
|
+
# 非组件关键词(生命周期/builder/容器等误判源)
|
|
53
|
+
NON_COMPONENT = re.compile(r'^(onPageShow|onBackPress|aboutToAppear|aboutToDisappear|'
|
|
54
|
+
r'\w*[Bb]uilder|TabContent|ForEach|LazyForEach|Repeat|'
|
|
55
|
+
r'Navigation|NavDestination|if|else|while|for|struct|class|build)\b')
|
|
56
|
+
# 风险权限(需用户确认/无法预授)
|
|
57
|
+
RISKY_PERMS = {'ohos.permission.USER_AUTH', 'ohos.permission.MANAGE_FINGERPRINT'}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def read(p):
|
|
61
|
+
try:
|
|
62
|
+
return open(p, encoding='utf-8', errors='ignore').read()
|
|
63
|
+
except OSError:
|
|
64
|
+
return ''
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_pages(root):
|
|
68
|
+
"""定位入口模块与页面集。"""
|
|
69
|
+
# 找含 UIAbility+loadContent 的模块(entry 或自定义)
|
|
70
|
+
modules = []
|
|
71
|
+
for cand in sorted(os.listdir(root)):
|
|
72
|
+
mdir = os.path.join(root, cand)
|
|
73
|
+
if os.path.isdir(mdir) and os.path.isdir(os.path.join(mdir, 'src', 'main')):
|
|
74
|
+
modules.append(cand)
|
|
75
|
+
elif os.path.isdir(mdir) and cand not in ('oh_modules', '.hypium_forge', 'AppScope', 'hvigor', 'build', 'doc'):
|
|
76
|
+
# 二级模块容器(features/commons 下可能有独立模块)
|
|
77
|
+
for sub in sorted(os.listdir(mdir)):
|
|
78
|
+
sdir = os.path.join(mdir, sub)
|
|
79
|
+
if os.path.isdir(os.path.join(sdir, 'src', 'main')):
|
|
80
|
+
modules.append(f'{cand}/{sub}')
|
|
81
|
+
|
|
82
|
+
pages = {}
|
|
83
|
+
nav_graph = {}
|
|
84
|
+
entry_module, entry_page = None, None
|
|
85
|
+
for m in modules:
|
|
86
|
+
mdir = os.path.join(root, m, 'src', 'main')
|
|
87
|
+
# Ability 的 loadContent
|
|
88
|
+
for dp, _d, files in os.walk(os.path.join(mdir, 'ets')):
|
|
89
|
+
for f in files:
|
|
90
|
+
if f.endswith('.ets'):
|
|
91
|
+
src = read(os.path.join(dp, f))
|
|
92
|
+
mm = re.search(r"loadContent\(\s*'([^']+)'", src)
|
|
93
|
+
if mm and entry_page is None:
|
|
94
|
+
entry_module, entry_page = m, mm.group(1)
|
|
95
|
+
# 路由表(可能多个)
|
|
96
|
+
for dp, _d, files in os.walk(os.path.join(mdir, 'resources')):
|
|
97
|
+
for f in files:
|
|
98
|
+
if f.endswith('_pages.json'):
|
|
99
|
+
try:
|
|
100
|
+
routes = json.load(open(os.path.join(dp, f))).get('src', [])
|
|
101
|
+
except (OSError, json.JSONDecodeError):
|
|
102
|
+
routes = []
|
|
103
|
+
for r in routes:
|
|
104
|
+
pages.setdefault((m, r), None)
|
|
105
|
+
# @Entry 页面兜底(覆盖路由表留下的 None 值)
|
|
106
|
+
ets_root = os.path.join(mdir, 'ets')
|
|
107
|
+
for dp, _d, files in os.walk(ets_root):
|
|
108
|
+
for f in files:
|
|
109
|
+
if f.endswith('.ets') and '@Entry' in read(os.path.join(dp, f)):
|
|
110
|
+
rel = os.path.relpath(os.path.join(dp, f), ets_root)[:-4]
|
|
111
|
+
if pages.get((m, rel)) is None:
|
|
112
|
+
pages[(m, rel)] = os.path.join(dp, f)
|
|
113
|
+
return modules, pages, entry_module, entry_page
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def scan_page(file_path):
|
|
117
|
+
"""单页扫描:交互点/状态/数据源/跳转。"""
|
|
118
|
+
src = read(file_path)
|
|
119
|
+
lines = src.split('\n')
|
|
120
|
+
interactions, states, navs = [], [], []
|
|
121
|
+
data_sources = [k for k, rx in DATA_HINTS.items() if rx.search(src)]
|
|
122
|
+
|
|
123
|
+
# 组件构造 + 邻近 .id()/文本
|
|
124
|
+
for i, ln in enumerate(lines):
|
|
125
|
+
m = re.match(r'^\s*(\w+)\s*(?:\(\s*(?:{[^}]*})?\s*\)?\s*(?:\{\s*)?)$', ln) or \
|
|
126
|
+
re.match(r'^\s*(Button|Text|Image|Toggle|TextInput)\s*\(', ln)
|
|
127
|
+
if not m or NON_COMPONENT.match(m.group(1)):
|
|
128
|
+
continue
|
|
129
|
+
comp = m.group(1)
|
|
130
|
+
# 向后 15 行找 .id / 文本内容 / onClick
|
|
131
|
+
after = '\n'.join(lines[i:min(len(lines), i + 15)])
|
|
132
|
+
idm = re.search(r"\.id\(\s*'([^']+)'", after)
|
|
133
|
+
txtm = re.search(r"(?:Text|Button)\s*\(\s*(?:this\.)?[\w.]*?'([^']{1,24})'", '\n'.join(lines[max(0,i-1):i+6]))
|
|
134
|
+
onclick = bool(re.search(r'\.on[A-Z]\w*\(', after))
|
|
135
|
+
if comp in INTERACTIVE_COMPONENTS or onclick:
|
|
136
|
+
target = idm.group(1) if idm else (txtm.group(1) if txtm else None)
|
|
137
|
+
if comp == 'Image' and not onclick:
|
|
138
|
+
continue
|
|
139
|
+
# 纯容器按行号定位且无文本无 id —— 噪声,剔除
|
|
140
|
+
if not target and comp in ('Column', 'Row', 'Stack', 'Flex', 'ListItem', 'List', 'Grid', 'Refresh', 'TabContent'):
|
|
141
|
+
continue
|
|
142
|
+
interactions.append({
|
|
143
|
+
'kind': 'click' if comp != 'TextInput' else 'input',
|
|
144
|
+
'target': target or f'line{i+1}:{comp}',
|
|
145
|
+
'desc': (txtm.group(1) if txtm else comp),
|
|
146
|
+
'has_id': bool(idm), 'line': i + 1, 'type': comp
|
|
147
|
+
})
|
|
148
|
+
# router 跳转
|
|
149
|
+
for nm in re.finditer(r"router\.(?:pushUrl|replaceUrl)\(\s*{\s*url:\s*'([^']+)'", after):
|
|
150
|
+
navs.append(nm.group(1))
|
|
151
|
+
|
|
152
|
+
# 条件状态(if 分支)
|
|
153
|
+
for i, ln in enumerate(lines):
|
|
154
|
+
if re.match(r'^\s*if\s*\(', ln):
|
|
155
|
+
cond = ln.strip()
|
|
156
|
+
states.append({'name': cond[:60], 'evidence': cond, 'line': i + 1})
|
|
157
|
+
return interactions, states[:20], navs, data_sources
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
ap = argparse.ArgumentParser()
|
|
162
|
+
ap.add_argument('harmony')
|
|
163
|
+
ap.add_argument('--out', default=None)
|
|
164
|
+
args = ap.parse_args()
|
|
165
|
+
root = args.harmony
|
|
166
|
+
|
|
167
|
+
app_json5 = os.path.join(root, 'AppScope', 'app.json5')
|
|
168
|
+
bundle = ''
|
|
169
|
+
m = re.search(r'"bundleName"\s*:\s*"([^"]+)"', read(app_json5))
|
|
170
|
+
if m:
|
|
171
|
+
bundle = m.group(1)
|
|
172
|
+
|
|
173
|
+
modules, pages, entry_module, entry_page = find_pages(root)
|
|
174
|
+
if not pages:
|
|
175
|
+
print('错误: 未找到任何页面(路由表或 @Entry)', file=sys.stderr)
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
# 权限(各模块 module.json5)
|
|
179
|
+
perms = []
|
|
180
|
+
for mdir in modules:
|
|
181
|
+
mj = os.path.join(root, mdir, 'src', 'main', 'module.json5')
|
|
182
|
+
perms += re.findall(r'"name"\s*:\s*"(ohos\.permission\.[\w.]+)"', read(mj))
|
|
183
|
+
perms = sorted(set(perms))
|
|
184
|
+
|
|
185
|
+
# 登录检测
|
|
186
|
+
has_login = False
|
|
187
|
+
for (mname, route), f in pages.items():
|
|
188
|
+
src = read(f) if f else ''
|
|
189
|
+
if any(h in src for h in LOGIN_HINTS):
|
|
190
|
+
# 有登录字样的页面
|
|
191
|
+
if re.search(r'登录|login|Login', src):
|
|
192
|
+
has_login = True
|
|
193
|
+
|
|
194
|
+
result = {'project_dir': os.path.abspath(root), 'bundle': bundle,
|
|
195
|
+
'entry': {'module': entry_module, 'page': entry_page},
|
|
196
|
+
'pages': [], 'permissions': perms, 'has_login': has_login,
|
|
197
|
+
'nav_graph': {}}
|
|
198
|
+
|
|
199
|
+
n_int = n_state = 0
|
|
200
|
+
for (mname, route), f in sorted(pages.items()):
|
|
201
|
+
if not f:
|
|
202
|
+
continue
|
|
203
|
+
interactions, states, navs, data_sources = scan_page(f)
|
|
204
|
+
result['pages'].append({
|
|
205
|
+
'page': os.path.basename(route), 'path': route, 'module': mname,
|
|
206
|
+
'file': os.path.abspath(f),
|
|
207
|
+
'nav_in': ['entry'] if route == entry_page else ['router:见 nav_graph'],
|
|
208
|
+
'interactions': interactions, 'states': states,
|
|
209
|
+
'data_sources': data_sources
|
|
210
|
+
})
|
|
211
|
+
result['nav_graph'][route] = sorted(set(navs))
|
|
212
|
+
n_int += len(interactions)
|
|
213
|
+
n_state += len(states)
|
|
214
|
+
|
|
215
|
+
# ── 深扫:多模块架构的真实 UI 在 features/commons 的 @Component 组件里 ──
|
|
216
|
+
seen_files = {p['file'] for p in result['pages']}
|
|
217
|
+
for m in modules:
|
|
218
|
+
ets_root = os.path.join(root, m, 'src', 'main', 'ets')
|
|
219
|
+
if not os.path.isdir(ets_root):
|
|
220
|
+
continue
|
|
221
|
+
for dp, _d, files in os.walk(ets_root):
|
|
222
|
+
for f in files:
|
|
223
|
+
if not f.endswith('.ets'):
|
|
224
|
+
continue
|
|
225
|
+
fp = os.path.join(dp, f)
|
|
226
|
+
if fp in seen_files:
|
|
227
|
+
continue
|
|
228
|
+
src = read(fp)
|
|
229
|
+
# 含 build() 的组件(@Component/@ComponentV2 struct)才算 UI 单元
|
|
230
|
+
if '@Component' in src and re.search(r'\bbuild\s*\(\s*\)', src):
|
|
231
|
+
interactions, states, navs, data_sources = scan_page(fp)
|
|
232
|
+
if not interactions and not states:
|
|
233
|
+
continue
|
|
234
|
+
rel = os.path.relpath(fp, os.path.join(root, m, 'src', 'main', 'ets'))[:-4]
|
|
235
|
+
result['pages'].append({
|
|
236
|
+
'page': os.path.splitext(f)[0], 'path': f'comp:{m}/{rel}',
|
|
237
|
+
'module': m, 'file': os.path.abspath(fp),
|
|
238
|
+
'nav_in': ['builder/comp'], 'interactions': interactions,
|
|
239
|
+
'states': states, 'data_sources': data_sources
|
|
240
|
+
})
|
|
241
|
+
n_int += len(interactions)
|
|
242
|
+
n_state += len(states)
|
|
243
|
+
|
|
244
|
+
payload = json.dumps(result, indent=2, ensure_ascii=False)
|
|
245
|
+
if args.out:
|
|
246
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
|
|
247
|
+
open(args.out, 'w', encoding='utf-8').write(payload)
|
|
248
|
+
print(f'扫描完成: {len(result["pages"])} 页面 / {n_int} 交互点 / {n_state} 状态 / '
|
|
249
|
+
f'{len(perms)} 权限 / 登录页={"有" if has_login else "无"} → {args.out}')
|
|
250
|
+
else:
|
|
251
|
+
print(payload)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == '__main__':
|
|
255
|
+
main()
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""upgrade_asserts.py — 存量用例断言批量升级(EXEC-041)。
|
|
4
|
-
|
|
5
|
-
目标:把「仅 exist」的用例批量升级为值级断言,让存量测试具备发现能力。
|
|
6
|
-
|
|
7
|
-
升级规则(保守机械,不造新值——只把已有期望变强):
|
|
8
|
-
R1 expect:"exist" + 文本型 target(非 snake_case id)→ expect:"equals"(同 target)
|
|
9
|
-
理由:equals 是 EQUALS 精确匹配,能抓 CONTAINS 命中的前缀污染(❄️Gallery 类)
|
|
10
|
-
R2 expect:"exist" + key 型 target → 保留(页面锚点合法),但在用例末尾若
|
|
11
|
-
仍无值级断言,追加 count 断言(target=该 key,value=1,唯一性)
|
|
12
|
-
R3 absent/checked 等已是值级,不动
|
|
13
|
-
每次升级在 note 里追加留痕(防驯化审计)。
|
|
14
|
-
|
|
15
|
-
用法: python3 upgrade_asserts.py [--cases cases.json] [--dry-run]
|
|
16
|
-
"""
|
|
17
|
-
import argparse
|
|
18
|
-
import json
|
|
19
|
-
import re
|
|
20
|
-
|
|
21
|
-
ID_LIKE = re.compile(r'^(tc_[a-z0-9_]+|[a-z][a-z0-9]*(_[a-z0-9]+)+|line\d+:\w+)$')
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def upgrade(cases, dry=False):
|
|
25
|
-
changed = 0
|
|
26
|
-
for c in cases:
|
|
27
|
-
has_value = any(s.get('expect') in
|
|
28
|
-
('equals', 'absent', 'text', 'text_contains', 'count',
|
|
29
|
-
'below', 'checked', 'unchecked', 'enabled', 'disabled', 'toast')
|
|
30
|
-
for s in c.get('steps', []))
|
|
31
|
-
for s in c.get('steps', []):
|
|
32
|
-
if s.get('expect') != 'exist':
|
|
33
|
-
continue
|
|
34
|
-
t = str(s.get('target', ''))
|
|
35
|
-
if t and not ID_LIKE.match(t):
|
|
36
|
-
# R1: 文本期望 → 精确
|
|
37
|
-
s['expect'] = 'equals'
|
|
38
|
-
c.setdefault('note', '')
|
|
39
|
-
c['note'] += f"[断言升级R1 exist→equals: {t[:20]}]"
|
|
40
|
-
changed += 1
|
|
41
|
-
elif t and ID_LIKE.match(t):
|
|
42
|
-
# R2: key 锚点保留,仅当全例无值级时补 count
|
|
43
|
-
if not has_value and s is c['steps'][-1]:
|
|
44
|
-
c['steps'].append({'action': '断言升级R2:控件唯一性', 'target': t,
|
|
45
|
-
'expect': 'count', 'value': '1'})
|
|
46
|
-
c['note'] = (c.get('note', '') + f"[断言升级R2 补count: {t[:20]}]")
|
|
47
|
-
changed += 1
|
|
48
|
-
return changed
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def main():
|
|
52
|
-
ap = argparse.ArgumentParser()
|
|
53
|
-
ap.add_argument('--cases', default='cases.json')
|
|
54
|
-
ap.add_argument('--dry-run', action='store_true')
|
|
55
|
-
args = ap.parse_args()
|
|
56
|
-
cases = json.load(open(args.cases, encoding='utf-8'))
|
|
57
|
-
n = upgrade(cases, args.dry_run)
|
|
58
|
-
if args.dry_run:
|
|
59
|
-
print(f'[dry-run] 将升级 {n} 处')
|
|
60
|
-
else:
|
|
61
|
-
json.dump(cases, open(args.cases, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
|
62
|
-
print(f'[written] 升级 {n} 处 → {args.cases}')
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if __name__ == '__main__':
|
|
66
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""upgrade_asserts.py — 存量用例断言批量升级(EXEC-041)。
|
|
4
|
+
|
|
5
|
+
目标:把「仅 exist」的用例批量升级为值级断言,让存量测试具备发现能力。
|
|
6
|
+
|
|
7
|
+
升级规则(保守机械,不造新值——只把已有期望变强):
|
|
8
|
+
R1 expect:"exist" + 文本型 target(非 snake_case id)→ expect:"equals"(同 target)
|
|
9
|
+
理由:equals 是 EQUALS 精确匹配,能抓 CONTAINS 命中的前缀污染(❄️Gallery 类)
|
|
10
|
+
R2 expect:"exist" + key 型 target → 保留(页面锚点合法),但在用例末尾若
|
|
11
|
+
仍无值级断言,追加 count 断言(target=该 key,value=1,唯一性)
|
|
12
|
+
R3 absent/checked 等已是值级,不动
|
|
13
|
+
每次升级在 note 里追加留痕(防驯化审计)。
|
|
14
|
+
|
|
15
|
+
用法: python3 upgrade_asserts.py [--cases cases.json] [--dry-run]
|
|
16
|
+
"""
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
ID_LIKE = re.compile(r'^(tc_[a-z0-9_]+|[a-z][a-z0-9]*(_[a-z0-9]+)+|line\d+:\w+)$')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def upgrade(cases, dry=False):
|
|
25
|
+
changed = 0
|
|
26
|
+
for c in cases:
|
|
27
|
+
has_value = any(s.get('expect') in
|
|
28
|
+
('equals', 'absent', 'text', 'text_contains', 'count',
|
|
29
|
+
'below', 'checked', 'unchecked', 'enabled', 'disabled', 'toast')
|
|
30
|
+
for s in c.get('steps', []))
|
|
31
|
+
for s in c.get('steps', []):
|
|
32
|
+
if s.get('expect') != 'exist':
|
|
33
|
+
continue
|
|
34
|
+
t = str(s.get('target', ''))
|
|
35
|
+
if t and not ID_LIKE.match(t):
|
|
36
|
+
# R1: 文本期望 → 精确
|
|
37
|
+
s['expect'] = 'equals'
|
|
38
|
+
c.setdefault('note', '')
|
|
39
|
+
c['note'] += f"[断言升级R1 exist→equals: {t[:20]}]"
|
|
40
|
+
changed += 1
|
|
41
|
+
elif t and ID_LIKE.match(t):
|
|
42
|
+
# R2: key 锚点保留,仅当全例无值级时补 count
|
|
43
|
+
if not has_value and s is c['steps'][-1]:
|
|
44
|
+
c['steps'].append({'action': '断言升级R2:控件唯一性', 'target': t,
|
|
45
|
+
'expect': 'count', 'value': '1'})
|
|
46
|
+
c['note'] = (c.get('note', '') + f"[断言升级R2 补count: {t[:20]}]")
|
|
47
|
+
changed += 1
|
|
48
|
+
return changed
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
ap = argparse.ArgumentParser()
|
|
53
|
+
ap.add_argument('--cases', default='cases.json')
|
|
54
|
+
ap.add_argument('--dry-run', action='store_true')
|
|
55
|
+
args = ap.parse_args()
|
|
56
|
+
cases = json.load(open(args.cases, encoding='utf-8'))
|
|
57
|
+
n = upgrade(cases, args.dry_run)
|
|
58
|
+
if args.dry_run:
|
|
59
|
+
print(f'[dry-run] 将升级 {n} 处')
|
|
60
|
+
else:
|
|
61
|
+
json.dump(cases, open(args.cases, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
|
62
|
+
print(f'[written] 升级 {n} 处 → {args.cases}')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
main()
|