Closed
fix(tests): 修复3个测试文件的mock问题 #14
gyangeAdd on Aug 29Closed at Aug 29
fix(tests): 修复3个测试文件的mock问题 #14
Closed
Merged fromgyange/CPTFfeature/device-native-driverintoCPF-RN/CPTFmain
Total: 475 files changed+102423-13450
| @@ -83,6 +83,76 @@ export const PLATFORM_REGISTRY: Record<Platform, PlatformMeta> = { | |||
| 83 | - **TypeScript strict mode**: All code must pass `tsc --noEmit` | 83 | - **TypeScript strict mode**: All code must pass `tsc --noEmit` |
| 84 | - **Async preferred**: Use async/await, avoid sync APIs except in tests | 84 | - **Async preferred**: Use async/await, avoid sync APIs except in tests |
| 85 | 85 | ||
| 86 | +## Concurrency Safety Strategy | ||
| 87 | + | ||
| 88 | +CPTF 在异步并发场景下采用三层防护策略,**大部分全局状态无需加锁**。 | ||
| 89 | + | ||
| 90 | +### 模式隔离原则(v0.3.x) | ||
| 91 | + | ||
| 92 | +模式(native/external)构造时定型,业务代码无 `if (isNativeMode)` 分支: | ||
| 93 | +- 启动期 1 次决策:`DriverModeArbiter.selectMode()` 决定整 run 模式 | ||
| 94 | +- 内部重试:L2 重连 / L3 action retry / L4 崩溃恢复 | ||
| 95 | +- 禁止操作期静默切实现(破坏隔离、报告难归因) | ||
| 96 | +- 详见 `cptf-native-driver/SKILL.md` 4 级降级策略 | ||
| 97 | + | ||
| 98 | +### Node.js 单线程原子性 | ||
| 99 | + | ||
| 100 | +Node.js 的事件循环是单线程的,这意味着: | ||
| 101 | +- **同步 Map 操作是原子的**:`Map.get()` / `Map.set()` / `Map.delete()` 不会被其他异步任务打断 | ||
| 102 | +- **两个 await 点之间不会发生交错**:代码块在单个 tick 内执行完毕 | ||
| 103 | + | ||
| 104 | +示例:`NavigationCoordinator` 的 13 个静态 Map(`lastModuleByPlatform`、`lastNavigateByDeepLinkByPlatform` 等)全部使用同步操作,无需 mutex 保护。 | ||
| 105 | + | ||
| 106 | +### 设备级隔离 | ||
| 107 | + | ||
| 108 | +Map 键设计遵循 `${platform}:${deviceId}` 格式: | ||
| 109 | + | ||
| 110 | +```typescript | ||
| 111 | +// NavigationCoordinator 使用设备组合键 | ||
| 112 | +static getModule(platform: string, deviceId: string): string | undefined { | ||
| 113 | + const key = `${platform}:${deviceId}`; | ||
| 114 | + return this.lastModuleByPlatform.get(key); | ||
| 115 | +} | ||
| 116 | +``` | ||
| 117 | + | ||
| 118 | +每个 `TestRunner` 实例绑定单个物理设备 → 不同设备之间绝不共享同一个 key → 并发安全自然保证。 | ||
| 119 | + | ||
| 120 | +### 并发模型与容错设计 | ||
| 121 | + | ||
| 122 | +| 层级 | 并发机制 | 共享状态 | 安全保证 | | ||
| 123 | +|------|---------|---------|---------| | ||
| 124 | +| `ParallelTestRunner` 多设备 | `Promise.all` 同进程 | 不同 deviceId | Map 键隔离 | | ||
| 125 | +| `TestRunner.run()` 多 case | `maxConcurrency`(默认 1) | 同 deviceId | 串行保护 | | ||
| 126 | +| `NavigationCoordinator` 静态 Map | 同步操作 | 全局 | 原子性 | | ||
| 127 | + | ||
| 128 | +**默认配置 `maxConcurrency = 1`** 保证同设备串行执行。即使启用并行(罕见场景),代码已有防御逻辑: | ||
| 129 | +- TestRunner.runCaseQueue 在 `maxConcurrency > 1` 时主动禁用 `setNextTest` | ||
| 130 | +- `shouldSkipDeepLink` 的启发式阈值(`failures >= 2`)容忍 off-by-one 偏差 | ||
| 131 | +- `deepLinkProbeInterval = 15` 提供足够缓冲空间 | ||
| 132 | + | ||
| 133 | +### 何时需要加锁 | ||
| 134 | + | ||
| 135 | +| 场景 | 是否需要锁 | 原因 | | ||
| 136 | +|------|-----------|------| | ||
| 137 | +| 同步 Map 操作 | ❌ 不需要 | Node.js 单线程保证原子性 | | ||
| 138 | +| 设备级隔离的状态 | ❌ 不需要 | 键空间不重叠 | | ||
| 139 | +| 初始化后只读的配置 | ❌ 不需要 | 无写入竞争 | | ||
| 140 | +| **多步骤操作含 await** | ✅ 需要 | 检查-执行间存在窗口期 | | ||
| 141 | +| **多进程资源共享** | ✅ 需要 | 跨进程边界失去原子性 | | ||
| 142 | +| **外部资源(文件、Redis)** | ✅ 需要 | 不在 Node.js 事件循环内 | | ||
| 143 | + | ||
| 144 | +示例:`ParallelTestRunner.buildQualityReportWithHook()` 执行 `spawn` 子进程 + 写入同一文件 → 需要文件锁。 | ||
| 145 | + | ||
| 146 | +### 设计原则 | ||
| 147 | + | ||
| 148 | +1. **信任同步原子性**:单线程模型天然防数据竞态 | ||
| 149 | +2. **键设计包含设备标识**:自动隔离不同设备的状态 | ||
| 150 | +3. **默认串行执行**:保守的并发策略 | ||
| 151 | +4. **启发式容错**:阈值设计容忍微小偏差 | ||
| 152 | +5. **显式清理**:`teardown()` 阶段重置所有状态 | ||
| 153 | + | ||
| 154 | +详细分析见 [`docs/architecture/CONCURRENCY-SAFETY.md`](../../../packages/framework/docs/architecture/CONCURRENCY-SAFETY.md)(含完整验证方法和案例分析)。 | ||
| 155 | + | ||
| 86 | ## Verification | 156 | ## Verification |
| 87 | 157 | ||
| 88 | ```bash | 158 | ```bash |
| @@ -100,14 +170,18 @@ find packages/framework/src -name "*.ts" -not -path "*/__tests__/*" -exec head - | |||
| 100 | - `cptf-test-api` — User-facing test API | 170 | - `cptf-test-api` — User-facing test API |
| 101 | - `cptf-refactor-rules` — v0.2.1+ 重构规则(Mixin 拆分 / as any 治理 / shell 标准化 / race 风暴 / 模板分离) | 171 | - `cptf-refactor-rules` — v0.2.1+ 重构规则(Mixin 拆分 / as any 治理 / shell 标准化 / race 风暴 / 模板分离) |
| 102 | 172 | ||
| 103 | -## v0.2.1 重构成果速查 | 173 | +## God class 拆分现状(协作类速查) |
| 104 | 174 | ||
| 105 | -| 类 | 拆前 | 拆后 | Mixin 数 | | 175 | +经 0.3.1 拆分,大型类的职责分布如下(具体行数以实际代码为准): |
| 106 | -|----|------|------|----------| | 176 | + |
| 107 | -| HarmonySystem | 1813 | 481 | 5 | | 177 | +| 类 | 拆分方式 | 协作类/模块 | |
| 108 | -| AndroidSystem | 1621 | 336 | 5 | | 178 | +|----|---------|------------| |
| 109 | -| IosSystem | 1087 | 457 | 5 | | 179 | +| TestRunner | 方法化 + 协作类拆出 | `core/case-queue-scheduler.ts`(SkipResultFactory + CircuitBreakerTracker)、`core/run-metrics-aggregator.ts`、`infrastructure/executor-loader.ts`;create 的 resolveRunDirectories/setupDriverMode/setupMetricServer 三 helper | |
| 110 | -| TestRunner | 1141 | 1103 | 2(HealthKeeper + ReportOrchestrator) | | 180 | +| ComparisonRunner | 流式对比重构 | StreamComparisonQueue(纯聚合器)+ saveReport 单出口 | |
| 111 | -| ComparisonRunner | 1116 | 1116 | 2(UIComparisonService + ComparisonReportBuilder) | | 181 | +| comparison-report-generator | facade + 三专职类 | comparison-html-sections / comparison-html-streaming / comparison-markdown-generator | |
| 182 | +| expect-executor | 纯函数拆出 | expect-element-utils / expect-color-analyzer | | ||
| 183 | +| test-scene-navigator | facade + 组合式 | scene-nav-shared / scene-nav-home / scene-nav-targets | | ||
| 184 | +| html-generator | 图表引擎抽出 | chart-renderer.ts | | ||
| 185 | +| HarmonySystem / AndroidSystem / IosSystem | Strategy mixin(Native/External 双子类) | mixins/ 目录 + host-contract.ts 宿主契约 | | ||
| 112 | 186 | ||
| 113 | **未来重构前必读 `cptf-refactor-rules`**,避免重蹈 race 风暴 / shell 重复 / as any 污染等 0.2.1 已修复的反模式。 | 187 | **未来重构前必读 `cptf-refactor-rules`**,避免重蹈 race 风暴 / shell 重复 / as any 污染等 0.2.1 已修复的反模式。 |
| @@ -1,6 +1,6 @@ | |||
| 1 | --- | 1 | --- |
| 2 | name: cptf-cli | 2 | name: cptf-cli |
| 3 | -description: Use when running cptf CLI commands, configuring parallel/multi-device execution, or interpreting CLI output. Triggers: "cptf run", "cptf compare", "cptf scan", "cptf devices", "cptf init", "cptf check-env", "cptf template", "--platform", "--module", "--strategy", "--device", "cptf.config.json", "parallel execution", "multi-device". Do NOT use for writing test cases (cptf-test-api) or for iOS specifics (cptf-ios). | 3 | +description: Use when running cptf CLI commands, configuring parallel/multi-device execution, or interpreting CLI output. Triggers: "cptf run", "cptf compare", "cptf scan", "cptf devices", "cptf init", "cptf check-env", "cptf template", "--platform", "--module", "--strategy", "--device", "--driver", "cptf.config.json", "parallel execution", "multi-device". Do NOT use for writing test cases (cptf-test-api) or for iOS specifics (cptf-ios). |
| 4 | --- | 4 | --- |
| 5 | 5 | ||
| 6 | # CPTF CLI Skill | 6 | # CPTF CLI Skill |
| @@ -19,6 +19,9 @@ cptf compare # cross-platform comparison | |||
| 19 | cptf init # create config file | 19 | cptf init # create config file |
| 20 | cptf check-env # check environment dependencies | 20 | cptf check-env # check environment dependencies |
| 21 | cptf template performance -n my-test # generate test template | 21 | cptf template performance -n my-test # generate test template |
| 22 | +cptf run --driver native # 灰盒模式(设备端原生执行,需测试 HAP) | ||
| 23 | +cptf run --driver external # 黑盒模式(外部 Agent,默认) | ||
| 24 | +cptf run --driver auto # 自动模式(优先 native,降级 external) | ||
| 22 | cptf run -p harmony --strategy round-robin --device <id> # 0.2.0+ | 25 | cptf run -p harmony --strategy round-robin --device <id> # 0.2.0+ |
| 23 | ``` | 26 | ``` |
| 24 | 27 | ||
| @@ -1,6 +1,6 @@ | |||
| 1 | --- | 1 | --- |
| 2 | name: cptf-framework-dev | 2 | name: cptf-framework-dev |
| 3 | -description: "Use when modifying CPTF framework internals (packages/framework) for v0.2.1+. Triggers: 'add platform', 'refactor runner', 'add metric collector', 'BaseSystem', 'TestRunner', 'ComparisonRunner', 'ParallelTestRunner', 'ISystem', 'PLATFORM_REGISTRY', 'IosSystem', 'HarmonySystem', 'AndroidSystem', 'SmartMonkey', 'WhiteScreenDetector', 'MemoryLeakDetector', 'SystemFactory', 'five-layer architecture', 'sub-interface', '@internal', 'TestExecutorInstance', 'StabilityTestConfig', 'drainHistories'. Do NOT use for writing user test cases (use cptf-test-api), CLI command usage (use cptf-cli), iOS WDA / xctrace (use cptf-ios), report customization (use cptf-report), or UI comparison config (use cptf-ui-comparison)." | 3 | +description: "Use when modifying CPTF framework internals (packages/framework) for v0.3.0+. Triggers: 'add platform', 'refactor runner', 'add metric collector', 'BaseSystem', 'TestRunner', 'ComparisonRunner', 'ParallelTestRunner', 'ISystem', 'PLATFORM_REGISTRY', 'IosSystem', 'HarmonySystem', 'AndroidSystem', 'SmartMonkey', 'WhiteScreenDetector', 'MemoryLeakDetector', 'SystemFactory', 'five-layer architecture', 'sub-interface', '@internal', 'TestExecutorInstance', 'StabilityTestConfig', 'drainHistories', 'HarmonyNativeDriver', 'Synchronizer', 'AppProbe', 'driverMode', 'native driver', 'selector-based ops', 'clickByTestId', 'inputTextByTestId', 'dual-mode optimization', '双模式优化', 'external vs native', 'performance optimization', '性能优化'. Do NOT use for writing user test cases (use cptf-test-api), CLI command usage (use cptf-cli), iOS WDA / xctrace (use cptf-ios), report customization (use cptf-report), or UI comparison config (use cptf-ui-comparison)." |
| 4 | --- | 4 | --- |
| 5 | 5 | ||
| 6 | # CPTF Framework Development Skill (Master Entry Point) | 6 | # CPTF Framework Development Skill (Master Entry Point) |
| @@ -10,20 +10,22 @@ description: "Use when modifying CPTF framework internals (packages/framework) f | |||
| 10 | > belong to any single sub-skill. For task-specific lookups, use the | 10 | > belong to any single sub-skill. For task-specific lookups, use the |
| 11 | > focused sub-skills instead. | 11 | > focused sub-skills instead. |
| 12 | > | 12 | > |
| 13 | -> | Sub-skill | Covers | When to use | | 13 | + > | Sub-skill | Covers | When to use | |
| 14 | -> |-----------|--------|-------------| | 14 | + > |-----------|--------|-------------| |
| 15 | -> | `cptf-arch` | Five-layer architecture, dependency rules, type locations, coding standards, PLATFORM_REGISTRY | "where should I put", "layer dependency", "type definition" | | 15 | + > | `cptf-arch` | Five-layer architecture, dependency rules, type locations, coding standards, PLATFORM_REGISTRY | "where should I put", "layer dependency", "type definition" | |
| 16 | -> | `cptf-refactor-rules` | v0.2.1 refactor constraints, mixin split rules, anti-patterns | Before any refactoring of packages/framework | | 16 | + > | `cptf-refactor-rules` | v0.2.1 refactor constraints, mixin split rules, anti-patterns | Before any refactoring of packages/framework | |
| 17 | -> | `cptf-system-extension` | New platform, ISystem sub-interfaces, mixin pattern, SystemFactory | "add platform", "new ISystem" | | 17 | + > | `cptf-system-extension` | New platform, ISystem sub-interfaces, mixin pattern, SystemFactory | "add platform", "new ISystem" | |
| 18 | -> | `cptf-test-api` | @Test/@Step decorators, TestConfig, StepConfig, ExpectConfig | Writing test cases | | 18 | + > | `cptf-test-api` | @Test/@Step decorators, TestConfig, StepConfig, ExpectConfig | Writing test cases | |
| 19 | -> | `cptf-ui-locator` | UIAutomation API, non-invasive locators, layout cache rules | "findByTestId", "clickByTestId", "locator strategy" | | 19 | + > | `cptf-ui-locator` | UIAutomation API, non-invasive locators, layout cache rules | "findByTestId", "clickByTestId", "locator strategy" | |
| 20 | -> | `cptf-ui-comparison` | UIComparisonConfig, UIComparator, cross-platform diff | "uiComparison", "visual comparison" | | 20 | + > | `cptf-ui-comparison` | UIComparisonConfig, UIComparator, cross-platform diff | "uiComparison", "visual comparison" | |
| 21 | -> | `cptf-stability` | StabilityTest, 9 detector params, StabilityTestConfig, crash recovery | "SmartMonkey", "WhiteScreenDetector", "MemoryLeakDetector" | | 21 | + > | `cptf-stability` | StabilityTest, 9 detector params, StabilityTestConfig, crash recovery | "SmartMonkey", "WhiteScreenDetector", "MemoryLeakDetector" | |
| 22 | -> | `cptf-cli` | CLI commands, parallel execution, config file, device allocation | "cptf run", "cptf compare", "--strategy" | | 22 | + > | `cptf-cli` | CLI commands, parallel execution, config file, device allocation | "cptf run", "cptf compare", "--strategy" | |
| 23 | -> | `cptf-ios` | iOS WDA, xctrace, simulator vs real device, Phase 0/1/2 | "WDA", "xctrace", "iOS real device" | | 23 | + > | `cptf-ios` | iOS WDA, xctrace, simulator vs real device, Phase 0/1/2 | "WDA", "xctrace", "iOS real device" | |
| 24 | -> | `cptf-report` | HTML/Markdown/JSON generators, ReportJobQueue, trend charts | "report format", "custom generator" | | 24 | + > | `cptf-report` | HTML/Markdown/JSON generators, ReportJobQueue, trend charts | "report format", "custom generator" | |
| 25 | -> | `cptf-troubleshoot` | Common issues, debug recipes, common mistakes table | "timeout", "element not found", "test hangs" | | 25 | + > | `cptf-troubleshoot` | Common issues, debug recipes, common mistakes table | "timeout", "element not found", "test hangs" | |
| 26 | -> | `cptf-rnoh-app` ⬆️ | App-side integration, scene components, Deep Link config (root `.opencode/skills/`) | "App.tsx", "scene component", "bundle:release" | | 26 | + > | `cptf-optimization-verification` | Static analysis false positive patterns, verification-first workflow, cost-benefit decision framework | Receiving optimization proposals from static analysis tools or code reviews | |
| 27 | + > | `cptf-rnoh-app` ⬆️ | App-side integration, scene components, Deep Link config (root `.opencode/skills/`) | "App.tsx", "scene component", "bundle:release" | | ||
| 28 | + > | **Dual-Mode Optimization Rule** *(本 skill 末段)* | **External vs Native 双模式优化约束、决策矩阵、反模式** | **任何性能/架构优化前必读** | | ||
| 27 | 29 | ||
| 28 | --- | 30 | --- |
| 29 | 31 | ||
| @@ -37,7 +39,7 @@ need `KNOWN_MULTI_EXPORT_FILES` and `SCENE_NAME_MAP` configured in the script. | |||
| 37 | 39 | ||
| 38 | - Location: `app/scripts/generate-scenes-index.js` | 40 | - Location: `app/scripts/generate-scenes-index.js` |
| 39 | - Trigger: `npm run bundle:release` → `generateScenesIndex` → overwrites manual edits | 41 | - Trigger: `npm run bundle:release` → `generateScenesIndex` → overwrites manual edits |
| 40 | -- Verification: `node generate-scenes-index.js` should output "12个场景" | 42 | +- Verification: `node generate-scenes-index.js` output should match the current scene count in `app/tests/scenes/index.ts` (grows with coverage; ~116 as of 2026-08) |
| 41 | 43 | ||
| 42 | ### SafeAreaView Nesting | 44 | ### SafeAreaView Nesting |
| 43 | 45 | ||
| @@ -59,12 +61,12 @@ Any specification change MUST update documentation: | |||
| 59 | 61 | ||
| 60 | | Change Type | Required Updates | | 62 | | Change Type | Required Updates | |
| 61 | |-------------|------------------| | 63 | |-------------|------------------| |
| 62 | -| API 新增/修改 | `docs/CAPABILITY-SPEC.md` + `README.md` | | 64 | +| API 新增/修改 | `docs/architecture/CAPABILITY-SPEC.md` + `README.md` | |
| 63 | -| 接口变化 | `docs/USER-GUIDE.md` | | 65 | +| 接口变化 | `docs/getting-started/USER-GUIDE.md` | |
| 64 | | 架构调整 | `docs/ARCHITECTURE.md` + `AGENTS.md` | | 66 | | 架构调整 | `docs/ARCHITECTURE.md` + `AGENTS.md` | |
| 65 | -| 类型定义 | `docs/CAPABILITY-SPEC.md` | | 67 | +| 类型定义 | `docs/architecture/CAPABILITY-SPEC.md` | |
| 66 | -| 性能指标 | `docs/CAPABILITY-SPEC.md` | | 68 | +| 性能指标 | `docs/architecture/CAPABILITY-SPEC.md` | |
| 67 | -| 命令格式 | `docs/CAPABILITY-SPEC.md` | | 69 | +| 命令格式 | `docs/architecture/CAPABILITY-SPEC.md` | |
| 68 | | 规格变更 | 补充/修改对应测试用例 | | 70 | | 规格变更 | 补充/修改对应测试用例 | |
| 69 | 71 | ||
| 70 | **Verification Checklist**: | 72 | **Verification Checklist**: |
| @@ -98,7 +100,7 @@ Any specification change MUST update documentation: | |||
| 98 | 100 | ||
| 99 | - 一致性 = expectResults 对比计数(matched/total) | 101 | - 一致性 = expectResults 对比计数(matched/total) |
| 100 | - 不计算比例,只看验证项是否全部一致 | 102 | - 不计算比例,只看验证项是否全部一致 |
| 101 | -- consistencyScore = 1.0(全部一致)或 0.0(有不一致) | 103 | +- consistencyScore = matched/total 比率(>= 0.8 判一致,< 0.5 为 critical) |
| 102 | 104 | ||
| 103 | **不参与一致性判断的指标**(仅作为告警显示): | 105 | **不参与一致性判断的指标**(仅作为告警显示): |
| 104 | - 执行成功与否、执行时长差异、FPS差异、errors差异 | 106 | - 执行成功与否、执行时长差异、FPS差异、errors差异 |
| @@ -142,7 +144,7 @@ input text 'text' | |||
| 142 | ```bash | 144 | ```bash |
| 143 | cd packages/framework | 145 | cd packages/framework |
| 144 | pnpm build # Compile TypeScript | 146 | pnpm build # Compile TypeScript |
| 145 | -pnpm test # Run Jest tests (696 tests) | 147 | +pnpm test # Run Jest tests |
| 146 | pnpm typecheck # tsc --noEmit | 148 | pnpm typecheck # tsc --noEmit |
| 147 | pnpm verify # Run scripts/verify.ts (architecture integrity checks) | 149 | pnpm verify # Run scripts/verify.ts (architecture integrity checks) |
| 148 | pnpm pack # Generate cptf-framework-*.tgz | 150 | pnpm pack # Generate cptf-framework-*.tgz |
| @@ -201,12 +203,16 @@ Code: `cli.ts` platform selection logic | |||
| 201 | 203 | ||
| 202 | ## Related Documents | 204 | ## Related Documents |
| 203 | 205 | ||
| 204 | -- `AGENTS.md`: Full project guide (keep ≤100 lines, details in sub-skills) | 206 | +- `AGENTS.md`: Full project guide (keep ≤150 lines, details in sub-skills; verify enforces this) |
| 205 | -- `packages/framework/docs/ARCHITECTURE.md`: Architecture details | 207 | +- `packages/framework/docs/architecture/ARCHITECTURE.md`: Architecture details |
| 206 | -- `packages/framework/docs/CAPABILITY-SPEC.md`: API and metrics specification | 208 | +- `packages/framework/docs/architecture/CAPABILITY-SPEC.md`: API and metrics specification |
| 207 | -- `packages/framework/docs/USER-GUIDE.md`: Quick start and usage guide | 209 | +- `packages/framework/docs/getting-started/USER-GUIDE.md`: Quick start and usage guide |
| 208 | -- `packages/framework/docs/DEVELOPMENT-GUIDE.md`: Extension development guide | 210 | +- `packages/framework/docs/development/DEVELOPMENT-GUIDE.md`: Extension development guide |
| 209 | -- `packages/framework/docs/analysis/OPTIMIZATION-ANALYSIS.md`: Optimization analysis (15/18 items done) | 211 | +- `packages/framework/CHANGELOG.md(0.2.1 优化记录已并入)`: Optimization analysis (15/18 items done) |
| 212 | +- `packages/framework/docs/architecture/CONCURRENCY-SAFETY.md`: **并发安全模型** — 为什么 NavigationCoordinator 等全局状态不需要加锁 | ||
| 213 | +- `packages/framework/docs/METRICS-SPEC.md`: **测试指标统一规范** — 用例三态模型、通过率公式、崩溃统计、跨格式一致性。所有报告生成器**必须**调用 `computeUnifiedMetrics(result)`,禁止各自计算。 | ||
| 214 | +- `packages/framework/docs/architecture/STABILITY-SPEC.md`: 稳定性设计与降级策略 | ||
| 215 | +- `packages/framework/docs/architecture/PERFORMANCE-METRICS-SPEC.md`: 性能指标采集规范 | ||
| 210 | 216 | ||
| 211 | ## Related Skills (Quick Routing) | 217 | ## Related Skills (Quick Routing) |
| 212 | 218 | ||
| @@ -222,3 +228,302 @@ Code: `cli.ts` platform selection logic | |||
| 222 | - **Report customization** → `cptf-report` | 228 | - **Report customization** → `cptf-report` |
| 223 | - **Debugging / common issues** → `cptf-troubleshoot` | 229 | - **Debugging / common issues** → `cptf-troubleshoot` |
| 224 | - **App integration (App.tsx, scenes)** → `cptf-rnoh-app`(根目录 `.opencode/skills/`) | 230 | - **App integration (App.tsx, scenes)** → `cptf-rnoh-app`(根目录 `.opencode/skills/`) |
| 231 | + | ||
| 232 | + | ||
| 233 | +## Architecture (v0.3.0+ — DeviceBridge Removed) | ||
| 234 | + | ||
| 235 | +### Three-Layer Architecture (Simplified) | ||
| 236 | + | ||
| 237 | +``` | ||
| 238 | +调度层: UIAutomation → ISystem (direct delegation, no intermediate strategy/bridge layer) | ||
| 239 | +系统层: ISystem 7 sub-interfaces → HarmonySystem/AndroidSystem/IosSystem | ||
| 240 | + HarmonySystem has: driverMode (shell|native) + HarmonyNativeDriver (internal WS client) | ||
| 241 | +基础层: types/config/logger/cache/port-management/device-discovery | ||
| 242 | +``` | ||
| 243 | + | ||
| 244 | +### Driver Mode 隔离 + 4 级降级原则(v0.3.0 已完成) | ||
| 245 | + | ||
| 246 | +**核心**:模式构造时定型 + 内部重试 + 禁止操作期静默切实现。 | ||
| 247 | + | ||
| 248 | +| 维度 | 状态(v0.3.0+)| | ||
| 249 | +|---|---| | ||
| 250 | +| 抽象层 | DeviceBridge/UIStrategy 已删除 | | ||
| 251 | +| `HarmonyNativeDriver` / `AndroidNativeDriver` | 内部组件 | | ||
| 252 | +| `driverMode` | 内部 config(`'shell'` / `'native'`)| | ||
| 253 | +| Mixin 路由 | **Strategy 模式 + 构造时定型**(6 mixin × 2 平台 = 12 个策略子类)| | ||
| 254 | +| 业务代码 | **零 `isNativeMode` runtime 分支** | | ||
| 255 | +| 失败处理 | 4 级降级(启动期/重连/重试/崩溃恢复)| | ||
| 256 | + | ||
| 257 | +**4 级降级表**(详见 `cptf-native-driver/SKILL.md`): | ||
| 258 | + | ||
| 259 | +| Level | 位置 | 切模式 | 范围 | 实现状态 | | ||
| 260 | +|---|---|---|---|---| | ||
| 261 | +| L1 Pre-flight | `ISystem.enableNativeDriver()`(v0.3.1 起装配下沉 system 层,TestRunner 只调接口) | ✅ 整 run 1 次 | run | ✅ 双平台已实现(Harmony 走 DriverModeArbiter) | | ||
| 262 | +| L2 Reconnect | WS 断开 | ❌ 仍 native | 单 case | ✅ 双平台已实现(指数退避 3 次) | | ||
| 263 | +| L3 Action Retry | `sendCommand()` 内部 | ❌ 仍 native | 单 action | ✅ 双平台已实现 | | ||
| 264 | +| L4 Crash Recovery | APP_ERROR | ❌ 仍 native | 单 case | ✅ 双平台已实现 + TestRunner 经 `ISystem.tryNativeCrashRecovery` 接入 | | ||
| 265 | + | ||
| 266 | +**禁止事项**: | ||
| 267 | +- 操作期静默切实现(破坏隔离、报告难归因) | ||
| 268 | +- 启动期降级后再次自动切回 native(模式抖动) | ||
| 269 | +- 报告 `source` 字段混用 native/external 两种语义 | ||
| 270 | + | ||
| 271 | +### 隔离实现模板 | ||
| 272 | + | ||
| 273 | +```ts | ||
| 274 | +abstract class HarmonyInputController extends BaseSystemMixin<HarmonySystem> { | ||
| 275 | + abstract click(x: number, y: number): Promise<boolean>; | ||
| 276 | + abstract clickByTestId(testID: string): Promise<boolean>; | ||
| 277 | + // ... 15 abstract 方法 | ||
| 278 | +} | ||
| 279 | +class NativeInputController extends HarmonyInputController { | ||
| 280 | + private get driver(): HarmonyNativeDriver { return this.host.nativeDriver as HarmonyNativeDriver; } | ||
| 281 | + async click(x, y) { return this.driver.click(x, y); } | ||
| 282 | + async clickByTestId(t) { return this.driver.clickComponent(t); } | ||
| 283 | +} | ||
| 284 | +class ExternalInputController extends HarmonyInputController { | ||
| 285 | + // HMD RPC + uitest shell 降级(仅 external 内部,不跨模式) | ||
| 286 | +} | ||
| 287 | + | ||
| 288 | +// HarmonySystem 构造时定型 | ||
| 289 | +this.input = config.platformOptions?.['driver'] === 'native' | ||
| 290 | + ? new NativeInputController(this) | ||
| 291 | + : new ExternalInputController(this); | ||
| 292 | +``` | ||
| 293 | + | ||
| 294 | +### Key Design Decisions | ||
| 295 | + | ||
| 296 | +1. **DeviceBridge layer REMOVED** — ExternalDeviceBridge was 100% delegation to ISystem (zero value-add), NativeDeviceBridge absorbed into HarmonyNativeDriver | ||
| 297 | +2. **UIStrategy layer REMOVED** — UIAutomation delegates directly to ISystem | ||
| 298 | +3. **ISystemUI extended** with selector-based ops: clickByTestId, inputTextByTestId, clearTextByTestId, scrollToTop/Bottom, assertComponentExist | ||
| 299 | +4. **driverMode** is an internal config on HarmonySystem: `'shell'` | `'native'` | ||
| 300 | +5. **HarmonyNativeDriver** is an internal component of HarmonySystem, not a separate abstraction | ||
| 301 | +6. **Layout cache** built into UIAutomation (TTL 1s) and HarmonyNativeDriver (TTL 1s) | ||
| 302 | +7. **6 mixin × 2 平台 = 12 策略子类**(input/layout/performance/media/navigation + 2 stability) | ||
| 303 | +8. **iOS 单实现**(无 native driver,6 mixin 全部单类,符合预期) | ||
| 304 | +9. **Android mixin 类名前缀**:所有 Native/External 子类带 `Android` 前缀(`AndroidNativeInputController` 而非 `NativeInputController`) | ||
| 305 | + | ||
| 306 | +### Driver Mode Routing(已通过 Strategy 模式解决) | ||
| 307 | + | ||
| 308 | +Native 模式:策略类 `NativeXxxController` 单路径调用 `nativeDriver.xxx()` | ||
| 309 | +External 模式:策略类 `ExternalXxxController` 内部 HMD RPC → uitest shell 二级降级(仅 external 内部,不跨模式) | ||
| 310 | + | ||
| 311 | +### Mixin Strategy Split(v0.3.0 已完成) | ||
| 312 | + | ||
| 313 | +**全部 6 个 mixin(harmony + android 双平台)已按 Native/External 策略类拆分,构造时定型**: | ||
| 314 | + | ||
| 315 | +| Mixin | 基类(abstract) | Harmony Native | Harmony External | Android Native | Android External | | ||
| 316 | +|-------|-----------------|----------------|------------------|----------------|------------------| | ||
| 317 | +| input | `HarmonyInputController` / `AndroidInputController` | `NativeInputController` | `ExternalInputController` | `AndroidNativeInputController` | `AndroidExternalInputController` | | ||
| 318 | +| layout | `HarmonyLayoutProvider` / `AndroidLayoutProvider` | `NativeLayoutProvider` | `ExternalLayoutProvider` | `AndroidNativeLayoutProvider` | `AndroidExternalLayoutProvider` | | ||
| 319 | +| performance | `HarmonyPerformanceProvider` / `AndroidPerformanceProvider` | `NativePerformanceProvider` | `ExternalPerformanceProvider` | `AndroidNativePerformanceProvider` | `AndroidExternalPerformanceProvider` | | ||
| 320 | +| media | `HarmonyMediaProvider` / `AndroidMediaProvider` | `NativeMediaProvider` | `ExternalMediaProvider` | `AndroidNativeMediaProvider` | `AndroidExternalMediaProvider` | | ||
| 321 | +| navigation | `HarmonyNavigationProvider` / `AndroidNavigationProvider` | `NativeNavigationProvider` | `ExternalNavigationProvider` | `AndroidNativeNavigationProvider` | `AndroidExternalNavigationProvider`(空壳)| | ||
| 322 | +| stability | `HarmonyStabilityProbe` / `AndroidStabilityProbe` | `NativeStabilityProbe`(空壳)| `ExternalStabilityProbe`(空壳)| `AndroidNativeStabilityProbe`(空壳)| `AndroidExternalStabilityProbe`(空壳)| | ||
| 323 | + | ||
| 324 | +**规则**: | ||
| 325 | +1. **禁止在 mixin 内使用 `isNativeMode` 分支**——Native/External 行为差异必须通过策略子类 override 实现 | ||
| 326 | +2. 策略类构造在 `HarmonySystem`/`AndroidSystem` 构造函数按 `driverMode` 定型(一次性) | ||
| 327 | +3. Native 策略内部**必须**提供 External 降级路径(nativeDriver 失败 → fallback 到基类 protected shell 实现) | ||
| 328 | +4. 模式无关逻辑(录屏、批量截图、network/storage 采集等)放基类 | ||
| 329 | +5. Native 策略中 nativeDriver 可能为 null(连接晚于构造)——需 null 防御 + try/catch 降级 | ||
| 330 | +6. **空壳子类可接受**:当 native/external 行为相同时(如 stability),子类为空壳;扩展 native 路径时填充 | ||
| 331 | + | ||
| 332 | +**设备端 metric 推送键名(getLatestMetric)**: | ||
| 333 | +- Harmony(NativeTestDriver.ets):`fps / memoryPeak / cpuPeak / cpuApp / fpsJank / fpsBigJank` | ||
| 334 | +- Android(DeviceDriver.java):`fps / pss / totalCpu / procCpu` | ||
| 335 | +- 两平台键名不同,Native 策略子类各自读取,禁止混用 | ||
| 336 | + | ||
| 337 | +**MetricSource 新增值**:`native-device`(Android 进程内 instrumentation 采集)、`native-hidumper`(Harmony 设备端 SP_daemon/hidumper)、`native-sp-daemon` | ||
| 338 | + | ||
| 339 | +**PerformanceCollector 推送链路**(v0.3.0+): | ||
| 340 | +- 构造时 `subscribeNativeMetricStream()` 自动调 `nativeDriver.onMetric(this.handleMetricEvent)` | ||
| 341 | +- 设备端 `startMonitoring` 定时 hidumper/gfxinfo → WS `metric` 事件 → `handleMetricEvent` → 直接入 `metricsHistory` | ||
| 342 | +- `collect()` 优先读 `lastPushedMetric`(≤2*intervalMs 内),不调 shell fallback | ||
| 343 | + | ||
| 344 | +**media-provider 特殊点**: | ||
| 345 | +- `screenshot()` 已拆分(Native 走 `driver.screenshot`,External 走 shell `screencap`) | ||
| 346 | +- `startScreenRecord/stopScreenRecord` **仍在基类**(两模式共享 shell `screenrecord` 路径,因 native 模式无独立录屏 API) | ||
| 347 | + | ||
| 348 | +--- | ||
| 349 | + | ||
| 350 | +## Dual-Mode Optimization Rule (MANDATORY) | ||
| 351 | + | ||
| 352 | +**核心原则**:任何架构重构、性能优化、接口调整都必须**同时分析 external 模式和 native 模式**的影响,禁止"只优化一种模式"的方案。 | ||
| 353 | + | ||
| 354 | +### 决策矩阵 | ||
| 355 | + | ||
| 356 | +| 优化类型 | External | Native | 实现策略 | | ||
| 357 | +|---------|----------|--------|---------| | ||
| 358 | +| 两种模式都受益 | ✅ | ✅ | **共享实现**(推荐) | | ||
| 359 | +| 仅 native 受益 | ❌ | ✅ | native-only 分支 + external 降级 | | ||
| 360 | +| 仅 external 受益 | ✅ | ❌ | external-only 分支 + native 不受影响 | | ||
| 361 | +| 两种模式互相冲突 | ❌ | ❌ | **禁止实现**,需重新设计 | | ||
| 362 | + | ||
| 363 | +### 实现检查清单(每次优化前必须完成) | ||
| 364 | + | ||
| 365 | +1. **路径分析**:当前代码路径在两种模式下是否都覆盖? | ||
| 366 | + - 共享路径 → 考虑共享实现 | ||
| 367 | + - 分支路径 → 是否需要分别优化? | ||
| 368 | +2. **接口兼容性**:优化后的接口/方法是否在两种模式下都可用? | ||
| 369 | + - 是 → 直接实现 | ||
| 370 | + - 否 → 提供降级或标记 TODO | ||
| 371 | +3. **数据一致性**:优化是否影响数据准确性? | ||
| 372 | + - 共享数据源(如 PerformanceCollector)→ 必须两种模式同时验证 | ||
| 373 | + - 独立数据源(如 WS 专用缓存)→ 可独立优化 | ||
| 374 | +4. **向后兼容**:是否破坏已有 API? | ||
| 375 | + - 两种模式都依赖的 API → 保持兼容 + 新增方法 | ||
| 376 | + - 单一模式依赖的 API → 可在该模式内部调整 | ||
| 377 | + | ||
| 378 | +### 反模式(禁止) | ||
| 379 | + | ||
| 380 | +| 反模式 | 问题 | 正确做法 | | ||
| 381 | +|--------|------|---------| | ||
| 382 | +| 只优化 native,external 性能下降 | external 用户体验降级 | 两种模式同时分析,共享优化或分别处理 | | ||
| 383 | +| 假设"只有 native 使用"而删除代码 | external 模式报错 | 检查所有调用点,两种模式都可能需要 | | ||
| 384 | +| 接口改名只改一种模式 | 另一种模式编译失败 | 接口变更必须两种模式同时支持 | | ||
| 385 | +| 性能优化只测一种模式 | 无法发现跨模式问题 | 两种模式都跑基准测试对比 | | ||
| 386 | +| 降级路径无条件破坏性清理 | 瞬时错误杀健康资源 | fallback 前先探测资源健康(`isAlive()`),只清理确认死亡的资源 | | ||
| 387 | +| 用"请求计数"刷新健康监测的活跃时间 | 持续失败的流量掩盖 staleness,健康监测永不触发 | `recordAttempt()` 只记尝试计数,`recordActivity()` 只在成功时刷新 `lastActivityAt` | | ||
| 388 | + | ||
| 389 | +## External 模式 U2/ATX 关键规则(实测血泪) | ||
| 390 | + | ||
| 391 | +1. **`dumpHierarchy()` 必须 `compressed=false`**:u2 agent 的 compressed dump **剔除 `hint` 属性**(placeholder/accessibilityHint 断言全灭)。未压缩 dump 含 hint(实测 49 个 vs 0 个)。 | ||
| 392 | +2. **ATX agent 死亡必须可自愈**(三层防护,缺一不可): | ||
| 393 | + - `ConnectionHealthMonitor.recordAttempt()` **不得刷新 `lastActivityAt`**(否则失败流量掩盖 staleness → 永不 ping → 永不重连;曾致 936 次连续降级) | ||
| 394 | + - `AndroidU2Client` 请求失败 streak ≥3 → 主动 `reconnect()`(`launchU2Server` 会先 `probeExistingService` 复用存活 agent,代价低) | ||
| 395 | + - layout fallback kill ATX 前:`!u2.reconnecting` guard(防 kill 刚重启的 agent 竞态)+ U2 瞬时失败先重试 1 次(200ms)再降级,agent 存活则返回缓存 | ||
| 396 | +3. **设备 `ps -A` 只显示 comm 名(15 字符)**:查 `app_process / EchoServer` 类进程必须 `ps -A -o PID,ARGS`(ARGS=完整 cmdline)。裸 `ps -A | grep X` 对 app_process 派生进程永远 miss。 | ||
| 397 | +4. **CLI 堆内存 re-exec 必须传播子进程退出码**(`src/cli/index.ts`):`execFileSync` 在子进程 exit≠0 时抛异常,若被外层 `catch {}` 吞掉会 fall-through 到 `program.parse()` → **父进程把整套用例重跑一遍**(有失败用例的 run 全部双倍耗时)。必须 `process.exit(e.status ?? 1)`。 | ||
| 398 | +5. **native 模式禁止启动 ATX agent**(`AndroidSystem._doInitU2` native 分支跳过):UiAutomation 全系统单例,ATX 注册失败后的残留进程会干扰 DeviceDriver 的 input 注入。 | ||
| 399 | + | ||
| 400 | + | ||
| 401 | + | ||
| 402 | +### 最佳实践案例 | ||
| 403 | + | ||
| 404 | +#### ✅ 案例 1:PerformanceCollector.takeIterationSnapshot (已实现) | ||
| 405 | + | ||
| 406 | +**问题**:每迭代调用 startMonitoring/stopMonitoring 造成 500ms WS 往返开销。 | ||
| 407 | + | ||
| 408 | +**优化**: | ||
| 409 | +- 新增 `takeIterationSnapshot()` 方法(获取快照但保持定时器) | ||
| 410 | +- `ExecutionEngine` 提升 start/stop 到 case 级别 | ||
| 411 | +- **两种模式都受益**: | ||
| 412 | + - Native:减少 WS 命令发送次数 | ||
| 413 | + - External:减少 timer 重建开销 | ||
| 414 | + | ||
| 415 | +**实现方式**:共享实现,无需分支。 | ||
| 416 | + | ||
| 417 | +#### ✅ 案例 2:collectPerformance 走 native driver (待实现) | ||
| 418 | + | ||
| 419 | +**问题**:collectPerformance() 调用 hdc shell 耗时 1-2s。 | ||
| 420 | + | ||
| 421 | +**优化**: | ||
| 422 | +- Native 模式:`HarmonyNativeDriver.collectMetrics()` 在设备端执行 shell,通过 WS 返回结果 | ||
| 423 | +- External 模式:保持原有 hdc shell 路径 | ||
| 424 | + | ||
| 425 | +**实现方式**:在 `HarmonySystem.collectPerformance()` 中判断 `isNativeMode` | ||
| 426 | +```typescript | ||
| 427 | +async collectPerformance() { | ||
| 428 | + if (this.isNativeMode && this.nativeDriver) { | ||
| 429 | + try { | ||
| 430 | + return await this.nativeDriver.collectMetrics(); // native-only | ||
| 431 | + } catch (e) { | ||
| 432 | + this.logger.debug(`Native collect failed, fallback: ${e.message}`); | ||
| 433 | + } | ||
| 434 | + } | ||
| 435 | + return this.collectPerformanceViaShell(); // external / fallback | ||
| 436 | +} | ||
| 437 | +``` | ||
| 438 | + | ||
| 439 | +**关键**:native-only 分支必须提供 external 降级,保证 external 模式不受影响。 | ||
| 440 | + | ||
| 441 | +#### ❌ 案例 3:Layout cache TTL 调整(需要两种模式同时验证) | ||
| 442 | + | ||
| 443 | +**问题**:考虑将 layout cache TTL 从 1s 提升到 5s。 | ||
| 444 | + | ||
| 445 | +**错误做法**:只测试 native 模式,发现性能提升 40%,直接合入。 | ||
| 446 | + | ||
| 447 | +**正确做法**: | ||
| 448 | +1. 分析两种模式的 cache 使用场景 | ||
| 449 | + - Native:频繁操作时 cache 命中率高,5s TTL 安全 | ||
| 450 | + - External:操作间隔长,5s TTL 可能导致 stale data | ||
| 451 | +2. 两种模式分别测试 | ||
| 452 | +3. 结果:External 在 5s TTL 下发现 30% stale data | ||
| 453 | +4. **决策**:保持 1s TTL,或两种模式使用不同 TTL | ||
| 454 | + | ||
| 455 | +### 验证清单(每次优化后必须完成) | ||
| 456 | + | ||
| 457 | +- [ ] **两种模式都跑基准测试**:对比优化前后的性能数据 | ||
| 458 | +- [ ] **检查所有调用点**:grep 查找优化代码的所有引用,确认两种模式都覆盖 | ||
| 459 | +- [ ] **数据准确性验证**:检查指标数据在两种模式下是否一致 | ||
| 460 | +- [ ] **向后兼容性验证**:确认已有 API 未破坏,或提供明确的迁移路径 | ||
| 461 | + | ||
| 462 | +--- | ||
| 463 | + | ||
| 464 | +### Removed Files (do not reference) | ||
| 465 | + | ||
| 466 | +| Removed | Replacement | | ||
| 467 | +|--------|-------------| | ||
| 468 | +| `system/native-device-bridge.ts` | `system/harmony/harmony-native-driver.ts` / `system/android/android-native-driver.ts` | | ||
| 469 | +| `system/external-device-bridge.ts` | Direct ISystem delegation in UIAutomation | | ||
| 470 | +| `system/device-bridge-factory.ts` | Not needed | | ||
| 471 | +| `capability/ui/ui-strategy.ts` | Not needed | | ||
| 472 | +| `schema/src/device-bridge.ts` | `schema/src/types.ts` (SwipeParams, Selector) | | ||
| 473 | + | ||
| 474 | +### Remaining Work | ||
| 475 | + | ||
| 476 | +| 项 | 优先级 | 状态 | | ||
| 477 | +|----|-------|------| | ||
| 478 | +| `metricWsServer` → `PerformanceCollector` 直连(当前经 `emitMetricEvent` 中转) | P1 | 待做 | | ||
| 479 | +| N1 Android 录屏根因(`nohup` 被 adb shell kill,已降级 warn) | P2 | 根因未修 | | ||
| 480 | +| N2 `success=true` 与迭代失败不一致 | P2 | 待调查 | | ||
| 481 | + | ||
| 482 | +## Native 模式 fallback 防御原则(v0.3.x) | ||
| 483 | + | ||
| 484 | +设计 Native/External 双子类时,**External 子类的 fallback 路径不应被 Native 子类继承**。常见反模式: | ||
| 485 | + | ||
| 486 | +```ts | ||
| 487 | +// ❌ 反模式:Native 子类继承 External fallback | ||
| 488 | +class NativeFindStrategy extends FindStrategy { | ||
| 489 | + async findByTestId(testID) { | ||
| 490 | + try { | ||
| 491 | + return await this.facade.findComponent(testID); // native 路径 | ||
| 492 | + } catch { | ||
| 493 | + return await this.fallback(); // ← 继承自 External 的 dump,native 模式也会触发! | ||
| 494 | + } | ||
| 495 | + } | ||
| 496 | +} | ||
| 497 | +``` | ||
| 498 | + | ||
| 499 | +```ts | ||
| 500 | +// ✅ 正解:native 模式快速返 null + 留 fallback 给调用方 | ||
| 501 | +class NativeFindStrategy extends FindStrategy { | ||
| 502 | + async findByTestId(testID) { | ||
| 503 | + try { | ||
| 504 | + const el = await this.facade.findComponent(testID); | ||
| 505 | + if (el) return el; | ||
| 506 | + return null; // ← 显式返回,不走 fallback | ||
| 507 | + } catch { | ||
| 508 | + return null; | ||
| 509 | + } | ||
| 510 | + } | ||
| 511 | +} | ||
| 512 | +``` | ||
| 513 | + | ||
| 514 | +### Native 模式必须 early-return 的检查清单 | ||
| 515 | + | ||
| 516 | +| 调用点 | 检查 | | ||
| 517 | +|------|------| | ||
| 518 | +| `System.warmup()` | native 必须跳过 daemon init(UitestRunner.init / AndroidU2Client.connect) | | ||
| 519 | +| `System._doInitXxx()` | native 必须跳过 daemon 启动(uitest/u2) | | ||
| 520 | +| `LayoutProvider.getUIElements()` | native 必须用 nativeDriver.dumpLayout,不能 fallback 到 system.dump | | ||
| 521 | +| `LayoutProvider.scrollContainer()` | native 必须用 driver.scrollToTop/Bottom,不能 fallback 到 shell swipe | | ||
| 522 | +| `LayoutProvider.waitForIdle()` | native 必须用 driver.waitForIdle,不能 fallback 到 healthMonitor 轮询 | | ||
| 523 | +| `findStrategy.findByText()` | native facade.findByText 失败直接 fallback getLayout(不要再 findComponents('*') 中间层) | | ||
| 524 | + | ||
| 525 | +### Native 模式验证标准 | ||
| 526 | + | ||
| 527 | +- typecheck + 776 tests 全过 | ||
| 528 | +- 设备端进程列表在 native 模式用例后**不包含** uitest daemon / u2 agent / wda runner | ||
| 529 | +- 报告字段 `result.driverMode` = 'native',`result.effectiveDriverMode` = 'native',`result.driverFallback` = false | ||
| @@ -66,7 +66,7 @@ Get simulator UDID: `xcrun simctl list devices -j | jq '.devices | to_entries | | |||
| 66 | 66 | ||
| 67 | ## xctrace FPS Collection | 67 | ## xctrace FPS Collection |
| 68 | 68 | ||
| 69 | -v0.2.0 优化 (`ios-system.ts:746+`): | 69 | +v0.2.0 优化(`IosSystem` FPS 采集): |
| 70 | - 串行 `xcrun xctrace` → **并发的 list + export**(`Promise.all`) | 70 | - 串行 `xcrun xctrace` → **并发的 list + export**(`Promise.all`) |
| 71 | - 复用 `BaseSystem.execCommand`(含 retry + ResilientConnection) | 71 | - 复用 `BaseSystem.execCommand`(含 retry + ResilientConnection) |
| 72 | - 2s 短窗缓存 `_xctraceFpsCache` 避免重复采集 | 72 | - 2s 短窗缓存 `_xctraceFpsCache` 避免重复采集 |
| @@ -0,0 +1,491 @@ | |||
| 1 | +--- | ||
| 2 | +name: cptf-native-driver | ||
| 3 | +description: Use when working on HarmonyNativeDriver, AndroidNativeDriver, NativeTestDriver.ets, DeviceDriver.java, ohosTest/instrumentation setup, or native driver mode (--driver native). Triggers: "HarmonyNativeDriver", "AndroidNativeDriver", "native driver", "ohosTest", "Driver.create", "NativeTestDriver", "DeviceDriver", "am instrument", "UiObject2", "By.res", "Component-direct", "clickComponent", "inputTextComponent", "waitForComponent", "rport", "adb reverse", "aa test", "wsl-usb". Do NOT use for external/shell mode only. | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# Native Driver Skill | ||
| 7 | + | ||
| 8 | +## Architecture | ||
| 9 | + | ||
| 10 | +``` | ||
| 11 | +WSL2 (宿主机) | ||
| 12 | + ├── HarmonyNativeDriver (WS Server :18081) | ||
| 13 | + │ ├── 布局缓存 (TTL 1s) | ||
| 14 | + │ ├── 截图缓存 (同 Step 复用) | ||
| 15 | + │ └── sendCommand → WS JSON-RPC (15s timeout) | ||
| 16 | + │ | ||
| 17 | + │ hdc rport tcp:18081 tcp:18081 | ||
| 18 | + │ | ||
| 19 | + └── 设备端 (ohosTest 进程) | ||
| 20 | + ├── Driver.create() (进程内, 不依赖 SA 5502 daemon) | ||
| 21 | + ├── NativeTestDriver.ets (WS Client + dispatch 25+ actions) | ||
| 22 | + └── Component 直接操作 (comp.click/inputText/clearText/scrollToTop...) | ||
| 23 | + | ||
| 24 | +入口: aa test -b <bundle> -m entry_test -s unittest OpenHarmonyTestRunner | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +## Key Principle | ||
| 28 | + | ||
| 29 | +**Driver.create() 必须在 ohosTest 进程内调用,不在应用进程。** | ||
| 30 | +应用进程同时跑 RNOH 渲染 + TestKit Driver 会互相竞争 → 失败。 | ||
| 31 | +ohosTest 进程通过 `delegator.startAbility(EntryAbility)` 启动应用,然后独立创建 Driver。 | ||
| 32 | + | ||
| 33 | +## NativeTestDriver.ets Action List | ||
| 34 | + | ||
| 35 | +| 类别 | Action | 说明 | | ||
| 36 | +|------|--------|------| | ||
| 37 | +| 坐标 | click/doubleClick/longClick/swipe/drag | 坐标操作 | | ||
| 38 | +| Component-direct | clickComponent/doubleClickComponent/longClickComponent | find+comp.click() 1往返 | | ||
| 39 | +| Component-direct | inputTextComponent/clearTextComponent | comp.inputText/clearText | | ||
| 40 | +| Component-direct | scrollToTop/scrollToBottom | comp.scrollToTop/Bottom | | ||
| 41 | +| 原生 | waitForComponent | driver.waitForComponent(on, timeout) | | ||
| 42 | +| 原生 | assertComponentExist | driver.assertComponentExist(on) | | ||
| 43 | +| 原生 | findComponents | driver.findComponents(on) | | ||
| 44 | +| 原生 | getComponentState | find+全属性(13个)1往返 | | ||
| 45 | +| 手势 | dragTo/pinchOut/pinchIn/scrollSearch | comp.dragTo/pinchOut/pinchIn/scrollSearch | | ||
| 46 | +| 按键 | triggerKey/pressBack/pressHome/wakeUpDisplay | driver.* | | ||
| 47 | +| 显示 | getDisplaySize/getDisplayRotation/setDisplayRotation | driver.* | | ||
| 48 | +| 布局 | dumpLayout | driver.dumpLayout(path) + cat | | ||
| 49 | +| 截图 | screenCap | driver.screenCap(path) | | ||
| 50 | +| 工具 | delayMs/waitForIdle/ping | driver.delayMs/waitForIdle | | ||
| 51 | + | ||
| 52 | +## HarmonyNativeDriver Methods | ||
| 53 | + | ||
| 54 | +宿主机侧方法,映射到 WS action: | ||
| 55 | + | ||
| 56 | +| 方法 | WS Action | 缓存 | | ||
| 57 | +|------|-----------|------| | ||
| 58 | +| click(x,y) | click | invalidateLayoutCache | | ||
| 59 | +| clickComponent(testID) | clickComponent | invalidateLayoutCache | | ||
| 60 | +| inputTextComponent(testID,text) | inputTextComponent | invalidateLayoutCache | | ||
| 61 | +| waitForComponent(testID,timeout) | waitForComponent | — | | ||
| 62 | +| getComponentState(testID) | getComponentState | — | | ||
| 63 | +| dumpLayout() | dumpLayout | — | | ||
| 64 | +| getLayout(forceRefresh) | dumpLayout | 布局缓存 TTL 1s | | ||
| 65 | +| screenshot(path) | screenCap | 截图缓存 | | ||
| 66 | +| waitForIdle(idleTime,timeout) | waitForIdle | — | | ||
| 67 | +| back()/home()/keyEvent() | pressBack/pressHome/triggerKey | invalidateLayoutCache | | ||
| 68 | + | ||
| 69 | +## HAR 包构建 | ||
| 70 | + | ||
| 71 | +```bash | ||
| 72 | +# 1. 注册 nativeharmony 模块到 app/harmony/build-profile.json5 | ||
| 73 | +# 2. 构建 HAR | ||
| 74 | +hvigorw assembleHar --mode module -p module=nativeharmony@default | ||
| 75 | + | ||
| 76 | +# 3. 引用 HAR(entry/oh-package.json5) | ||
| 77 | +"@cptf/native-harmony": "file:../../../cptf/packages/native-harmony/.../nativeharmony.har" | ||
| 78 | + | ||
| 79 | +# 4. List.test.ets | ||
| 80 | +import { runNativeTestDriver } from '@cptf/native-harmony'; | ||
| 81 | +export default function testsuite() { | ||
| 82 | + runNativeTestDriver('com.tester.RNTester', 'EntryAbility'); | ||
| 83 | +} | ||
| 84 | +``` | ||
| 85 | + | ||
| 86 | +## WSL2 环境 | ||
| 87 | + | ||
| 88 | +- `dev-init.sh` 自动建立 `hdc rport tcp:18081 tcp:18081` | ||
| 89 | +- mirrored 模式 127.0.0.1 loopback 共享 | ||
| 90 | +- `systemd: wsl-adb-server.service` + `wsl-hdc-server.service` | ||
| 91 | + | ||
| 92 | +## 常见问题 | ||
| 93 | + | ||
| 94 | +| 问题 | 原因 | 解决 | | ||
| 95 | +|------|------|------| | ||
| 96 | +| Driver.create() 失败 | 在应用进程内调用 | 改在 ohosTest 进程(aa test 启动) | | ||
| 97 | +| WS 连接失败 | rport 未建立 | `hdc -t <dev> rport tcp:18081 tcp:18081` | | ||
| 98 | +| deviceCap 失败 | /data/local/tmp 无写入权限 | 应用沙箱路径 | | ||
| 99 | +| Beta 固件 daemon 失败 | SA 5502 不暴露 apiCaller | 正式版固件(但 ohosTest 进程内 Driver.create 可绕过) | | ||
| 100 | +| HAR file: 引用失败 | hvigor OHM 解析器不支持源码目录 | 必须打包 .har 文件引用 | | ||
| 101 | +| **ohosTest connect timeout (30s)** | **external 模式部署清除了设备上的 entry_test 模块**(`deployHarmony:external:release` 只装主 HAP,覆盖安装时 ohosTest HAP 被移除;native 模式依赖它) | `node scripts/build/install-ohos-test.js` 或 `pnpm deployHarmony:native:release` 重装;框架超时报错已带此诊断(bm dump 查 entry_test → 给安装指引);确认命令:`hdc -t <dev> shell bm dump -n <bundle> | grep -c entry_test`(0=缺失) | | ||
| 102 | +| rport 残留导致连接假死 | 上次运行的 Reverse 转发还挂着,host 侧已无监听 | `hdc -t <dev> fport rm tcp:18081 tcp:18081`;框架超时诊断已提示此命令 | | ||
| 103 | + | ||
| 104 | +**部署模式纪律**:external/native 模式切换后必须用对应 deploy 命令重装——external 装的包 native 跑不了(无 entry_test),反之 native 装的包 external 也能跑(多装不冲突)。CI 流水线切换驱动模式时此为首要检查项。 | ||
| 105 | + | ||
| 106 | +## 4级降级策略(v0.3.x 隔离原则) | ||
| 107 | + | ||
| 108 | +### 核心原则 | ||
| 109 | + | ||
| 110 | +**模式启动期定型 + 内部重试 + 禁止操作期静默切实现** | ||
| 111 | + | ||
| 112 | +- ✅ 启动期可 1 次决策整 run 切模式(Level 1) | ||
| 113 | +- ✅ run 中 WS 断开、action 超时、app 崩溃 → 内部重试/恢复(Level 2/3/4) | ||
| 114 | +- ❌ 禁止 action 级别静默从 native 切到 external(破坏隔离、报告难归因) | ||
| 115 | + | ||
| 116 | +### 4 级降级表 | ||
| 117 | + | ||
| 118 | +| Level | 位置 | 触发条件 | 动作 | 切模式 | 范围 | | ||
| 119 | +|---|---|---|---|---|---| | ||
| 120 | +| **L1 Pre-flight** | `HarmonyNativeDriver.connect()` | WS 起不来 / ohosTest 启动失败 / Driver.create 失败 / AAMS 不可见 / 30s 内 WS 未连上 | `DriverModeArbiter` 决策:native 失败 → 整 run 切 external(需 `driverFallback=true`)| ✅ 整 run 1 次 | run | | ||
| 121 | +| **L2 Reconnect** | run 中 WS 断开 | `close` 事件(非主动 disconnect)| `tryReconnect()`:指数退避 1s/2s/4s × 3 次,30s 预算 | ❌ 仍 native | 单 case | | ||
| 122 | +| **L3 Action Retry** | `sendCommand()` 内部 | `isRetryableError()` 命中(NETWORK/TIMEOUT/transient DEVICE_ERROR)| 重试 1 次(200-500ms 间隔),失败抛 `ActionError` | ❌ 仍 native | 单 action | | ||
| 123 | +| **L4 Crash Recovery** | case 内 action 失败 | `APP_ERROR`(app crash / AAMS 丢窗口)| stopApp → launchApp → relaunchOhosTest → 等 WS 重连 → 重试当前 action | ❌ 仍 native | 单 case(1 次预算)| | ||
| 124 | + | ||
| 125 | +### 实现模板 | ||
| 126 | + | ||
| 127 | +**L1 启动期决策**(`DriverModeArbiter`,`system/harmony/driver-mode-arbiter.ts`): | ||
| 128 | +```ts | ||
| 129 | +class DriverModeArbiter { | ||
| 130 | + async arbitrate(): Promise<ModeArbitrationResult> { | ||
| 131 | + if (!this.nativeDriver) { | ||
| 132 | + return { selectedMode: 'external', effectiveMode: 'external', fallback: false }; | ||
| 133 | + } | ||
| 134 | + try { | ||
| 135 | + await this.nativeDriver.connect(); // 30s timeout | ||
| 136 | + return { selectedMode: 'native', effectiveMode: 'native', fallback: false }; | ||
| 137 | + } catch (e) { | ||
| 138 | + if (!this.config.driverFallback) throw e; | ||
| 139 | + this.logger.warn(`Native 启动失败,降级 external: ${e.message}`); | ||
| 140 | + this._remountMixins('external'); // 重构造 input + layout 策略 | ||
| 141 | + return { selectedMode: 'native', effectiveMode: 'external', fallback: true, reason: e.message }; | ||
| 142 | + } | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + // 注意:当前实现只 remount input + layout 两个 mixin | ||
| 146 | + // perf/media/stability/navigation 不在 remount 范围内(其自身有降级) | ||
| 147 | +} | ||
| 148 | +``` | ||
| 149 | + | ||
| 150 | +**L2 重连预算**(`HarmonyNativeDriver.tryReconnect()`): | ||
| 151 | +```ts | ||
| 152 | +async tryReconnect(): Promise<boolean> { | ||
| 153 | + for (let i = 0; i < 3; i++) { | ||
| 154 | + await sleep(1000 * Math.pow(2, i)); // 1s, 2s, 4s | ||
| 155 | + if (this._destroyed) return false; | ||
| 156 | + try { | ||
| 157 | + this.launchOhosTest(); | ||
| 158 | + if (await this.waitWSConnection(5000)) return true; | ||
| 159 | + } catch (e) { /* log */ } | ||
| 160 | + } | ||
| 161 | + return false; // 30s 预算耗尽 | ||
| 162 | +} | ||
| 163 | +``` | ||
| 164 | + | ||
| 165 | +**L3 操作重试**(`HarmonyNativeDriver.sendCommand()`): | ||
| 166 | +```ts | ||
| 167 | +try { | ||
| 168 | + return await attempt(); | ||
| 169 | +} catch (err) { | ||
| 170 | + if (isRetryableError(err) && !this._destroyed) { | ||
| 171 | + await sleep(isTimeoutError(err) ? 500 : 200); | ||
| 172 | + return await attempt(); // 1 次 | ||
| 173 | + } | ||
| 174 | + throw err; | ||
| 175 | +} | ||
| 176 | +``` | ||
| 177 | + | ||
| 178 | +**L4 崩溃恢复**(`HarmonyNativeDriver.recoverFromCrash(bundleId)`): | ||
| 179 | +``` | ||
| 180 | +case 进入 → checkpoint | ||
| 181 | + action 失败 (APP_ERROR) | ||
| 182 | + ↓ | ||
| 183 | + 1. host.stopApp(bundleId) + sleep 1s | ||
| 184 | + 2. host.launchApp(bundleId) + sleep 3s | ||
| 185 | + 3. this.tryReconnect() | ||
| 186 | + 4. 失败 → case 标 failed,继续下一 case(每 case 1 次预算) | ||
| 187 | +``` | ||
| 188 | + | ||
| 189 | +### 隔离架构(v0.3.0 已完成) | ||
| 190 | + | ||
| 191 | +**实现状态**:6 mixin × 2 平台 Strategy 模式全部完成,0 处 `isNativeMode` runtime 分支。 | ||
| 192 | + | ||
| 193 | +**实现模板**(当前实际代码): | ||
| 194 | +```ts | ||
| 195 | +// system/harmony/mixins/input-controller.ts | ||
| 196 | +abstract class HarmonyInputController extends BaseSystemMixin<HarmonySystem> { | ||
| 197 | + abstract click(x: number, y: number): Promise<boolean>; | ||
| 198 | + abstract clickByTestId(testID: string): Promise<boolean>; | ||
| 199 | + // ... 15 abstract 方法 | ||
| 200 | +} | ||
| 201 | + | ||
| 202 | +class NativeInputController extends HarmonyInputController { | ||
| 203 | + private get driver(): HarmonyNativeDriver { | ||
| 204 | + return this.host.nativeDriver as HarmonyNativeDriver; // 关键:从 host 读取,不通过构造器注入 | ||
| 205 | + } | ||
| 206 | + async click(x, y) { | ||
| 207 | + try { return await this.driver.click(x, y); } | ||
| 208 | + catch (e) { /* log + return false */ } | ||
| 209 | + } | ||
| 210 | + async clickByTestId(t) { | ||
| 211 | + try { return await this.driver.clickComponent(t); } | ||
| 212 | + catch (e) { /* log + return false */ } | ||
| 213 | + } | ||
| 214 | +} | ||
| 215 | + | ||
| 216 | +class ExternalInputController extends HarmonyInputController { | ||
| 217 | + async click(x, y) { | ||
| 218 | + if (this.useDriver) { /* HMD RPC */ } | ||
| 219 | + return await this.touchTap(x, y); /* shell fallback 仅 external 内部 */ | ||
| 220 | + } | ||
| 221 | + async clickByTestId(t) { | ||
| 222 | + const el = await this.host.layout.getElementByTestId(t, 3000); | ||
| 223 | + return el ? await this.click(el.bounds.center.x, el.bounds.center.y) : false; | ||
| 224 | + } | ||
| 225 | +} | ||
| 226 | + | ||
| 227 | +// system/harmony/harmony-system.ts(构造时定型) | ||
| 228 | +this.input = config.platformOptions?.['driver'] === 'native' | ||
| 229 | + ? new NativeInputController(this) | ||
| 230 | + : new ExternalInputController(this); | ||
| 231 | +``` | ||
| 232 | + | ||
| 233 | +**Android 类名前缀**:`AndroidNativeInputController` / `AndroidExternalInputController`(注意 `Android` 前缀) | ||
| 234 | + | ||
| 235 | +业务代码 `await system.input.click(x, y)` 永远单一实现,**无 if 分支**。 | ||
| 236 | + | ||
| 237 | +### 报告标注 | ||
| 238 | + | ||
| 239 | +``` | ||
| 240 | +[run] | ||
| 241 | + selected_mode = native | ||
| 242 | + effective_mode = native (or 'external (L1 fallback)') | ||
| 243 | + native_reconnects = 2 # L2 | ||
| 244 | + native_action_retries = 5 # L3 | ||
| 245 | + native_crash_recoveries = 0 # L4 | ||
| 246 | +``` | ||
| 247 | + | ||
| 248 | +每 case 标注 `mode_source: native-WS | native-shell | external-RPC | external-shell`,便于归因。 | ||
| 249 | +**注意**:此字段当前未实现到 TestResult 类型,需在 v0.3.x 后续工作中添加。 | ||
| 250 | + | ||
| 251 | +### 禁止事项 | ||
| 252 | + | ||
| 253 | +- ❌ 操作期静默从 native 切到 external(破坏隔离) | ||
| 254 | +- ❌ 启动期降级后再次自动切回 native(避免模式抖动) | ||
| 255 | +- ❌ 报告里 `source` 字段混用 native/external 两种语义 | ||
| 256 | +- ❌ L2/L3/L4 重试预算超限后还强行继续(必须 fail case) | ||
| 257 | + | ||
| 258 | +### 性能约束(禁止修改项) | ||
| 259 | + | ||
| 260 | +**forceRefresh 穿透保证(最高优先)**:`getUIElements(forceRefresh=true)` 必须绕过缓存发起新 fetch——R6/R7 的 waitForTestId/waitForElementDisappear 轮询依赖它。曾因 Strategy 拆分把缓存守卫复制进 Native/External 子类时多带一行 `return cached`,两分支等价导致 forceRefresh 变死参数,轮询全读陈旧布局。修改任何子类缓存逻辑后,必须验证 forceRefresh 穿透(连续两次 `getUIElements(true)` 应触发两次底层 fetch)。 | ||
| 261 | + | ||
| 262 | +以下项经过架构分析确认,**不应随意调整**,修改会破坏 External 模式或引入回归: | ||
| 263 | + | ||
| 264 | +| 项 | 当前值 | 文件 | 禁止修改原因 | | ||
| 265 | +|----|--------|------|------------| | ||
| 266 | +| `LAYOUT_FETCH_TIMEOUT` | Harmony **35s** / Android 5s | `HarmonyLayoutProvider.LAYOUT_FETCH_TIMEOUT` | 外层 `Promise.race` 包裹了 `_getUIElementsInner` 的全部 3 条路径(Native + UITest RPC + Shell 回退)。UITest RPC 路径单次 15s × 2 retries + fallback = 33s 上限。**若缩短到 5s,External Harmony 模式下所有 captureLayout 都会被外层超时截断,导致元素查找永久失败。** 如要优化,必须从内层 `tryCaptureLayout` 的重试策略下手(如 2 retries → 1 retry,15s → 8s),外层 cap 保持不动 | | ||
| 267 | +| **清理时机:category 分组**(原"每用例 shell 清理"约束已被更优方案取代) | category 切换 + run 结束 | `orchestration/test-runner.ts` `cleanupRemoteTempFiles()` | 布局文件堆积问题已从根因解决:`dumpLayoutViaShell()` 合并命令**前置 rm** 上一次的 layout 文件,`/data/local/tmp` 任意时刻至多 1 个 layout_*.json,`ls` 性能不会劣化。截图/录屏在**创建时**即加入追踪列表(`addRemoteTempFile` 在 capture 之后、pull 之前),失败 pull 也被精确追踪。category 内不清理(0 shell),切换时 1 条精确 rm + 1 条模式感知残留 rm(Native 省略 layout_*.json) | | ||
| 268 | +| `withHdcMutex` 全局串行 | 所有 hdc shell | `HarmonySystem.withHdcMutex()` | **hdc 工具在 HarmonyOS 上不支持并发 spawn**,并行会导致设备端命令冲突、输出串流、连接不稳定。input/layout/perf/stability 四个 mixin 均经此串行。Native 模式主要操作已绕过,仅性能/稳定性采集仍受影响。采集由 `setInterval` 串行定时(默认 500-3000ms),去掉 mutex 也不会并行。**正确优化方向是通过 Native WS 推送指标,绕过 hdc shell** | | ||
| 269 | + | ||
| 270 | +**性能已修复项**(记录在此避免误改回): | ||
| 271 | + | ||
| 272 | +- ✅ `NativeUIStrategy` 3 次往返瓶颈:已通过重构绕过 `NativeUIStrategy`/`ExternalUIStrategy`,`UIAutomation` 直接委托 `ISystem.clickByTestId`,Native 模式 1 WS round-trip(~30-50ms) | ||
| 273 | +- ✅ `NativeDeviceBridge` 单队列串行:已删除该类,新 `HarmonyNativeDriver.sendCommand()` 无 `actionQueue`,每个请求独立 `Promise + PendingRequest` map | ||
| 274 | +- ⛔ 已删除 `ui-strategy.ts`(410 行死代码) | ||
| 275 | + | ||
| 276 | +**运行时修复项**: | ||
| 277 | + | ||
| 278 | +- ✅ **Native 模式 bundleId 未设置**:Native 分支跳过 `stopApp`/`launchApp` 时,也必须调用 `system.setCurrentApp(bundleId)`,否则 `_currentBundleId` 为空 → `ensurePid()` 失败 → 性能采集 0 条数据 + `pidof`/`ps` 全部空参数执行 → 每次 collect 阻塞 3s 触发 drain 超时 | ||
| 279 | +- ✅ **WS 意外关闭未 reject pending**(`HarmonyNativeDriver` 的 WS close handler):ohosTest 进程在设备上崩溃/退出 → WS close 事件必须调用 `rejectAllPending()` 立即 reject 所有 pending `sendCommand()`。原代码只设 `_connected=false`,导致 pending 调用傻等 120s 超时,外层测试循环卡死;非主动 disconnect 时必须走此路径。主动 `disconnect()` 时 `_destroyed=true`,`rejectAllPending` 已在 `disconnect()` 内调用,close handler 跳过第二次调用避免重复 | ||
| 280 | +- ✅ **ResilientConnection connectFn 无超时**(`base/utils/resilient-connection.ts`):`connectFn()`/`isConnectedFn()` 全部经 `withConnTimeout()` 包裹(默认 30s,`connectTimeoutMs` 可配)。TCP 半开连接挂起时不再永久阻塞 reconnect 循环 | ||
| 281 | +- ✅ **MetricWebSocketServer 帧重组合**(`base/utils/metric-ws-server.ts`):`MetricSocket.recvBuffer` per-socket 缓冲,跨 TCP `data` 事件的不完整帧保留至下次拼接。>1.4KB 消息分段不再丢失。溢出保护 `MAX_RECV_BUFFER_BYTES = 1MB`(畸形流销毁连接)。热路径零拷贝(recvBuffer 空时直接用 buf) | ||
| 282 | +- ✅ **unhandledRejection 处理器只生效一次**(`base/utils/logger.ts`):`process.once` → `process.on`(仅 uncaughtException/unhandledRejection;SIGINT/SIGTERM 保持 once——信号只处理一次后 exit) | ||
| 283 | +- ✅ **dumpLayout ls/cat 四连 shell → 单条合并命令**(`layout-provider.ts` `dumpLayoutViaShell()`):`rm 旧文件; uitest dumpLayout; 设备内轮询 ls+cat` 合并为 1 次 hdc 调用(原 4 次:dumpLayout + smartWaiter 轮询 ls + ls -t + cat)。前置 rm 保证 `ls` 找到的必然是本次产物(**精确**,不会捡到陈旧文件);文件生成等待在设备 shell 内 sleep 0.3 × 10 重试(不再每次 spawn hdc 轮询);uitest stdout/stderr 重定向 /dev/null 防止污染 cat 输出。布局文件自清理:任意时刻至多 1 个 | ||
| 284 | +- ✅ **清理 shell 按 category 分组**(`test-runner.ts` `cleanupRemoteTempFiles()`):每用例 0 条清理 shell → category 切换 + run 结束各 1 次(~10-15 次替代 67 次,省 ~28s/67 用例)。两道清理:①追踪列表精确 rm(截图/录屏**创建时**追踪——`addRemoteTempFile` 移到 capture 之后 pull 之前,pull 失败也覆盖)②模式感知残留 rm(Native 省略 layout_*.json)。`cleanupRemoteScreenshots` 内部 `rm -f f1 && rm -f f2` 链改为单条 `rm -f f1 f2 f3`(多文件一条 shell) | ||
| 285 | +- ✅ **健康检查时间门控**(`test-runner.ts`):`HEALTH_CHECK_INTERVAL_MS = 10s` + `lastHealthCheckOk` 跟踪。上次健康→10s 间隔才查(functional 用例 ~5s 一个,每 2 个查 1 次);上次不健康→跳过门控立即复查(恢复后不会连环失败)。67 用例从 67 次降到 ~7-10 次(省 ~29s) | ||
| 286 | +- ✅ **PID 缓存 TTL 30s → 60s**(`harmony-system.ts`):性能用例单次 25-30s,60s TTL 保证一个用例内只查 1 次;崩溃恢复路径 `clearCurrentApp()` 清缓存兜底 | ||
| 287 | +- ✅ **tryFindByTestId 冗余 dump 移除**(`ui-automation.ts`):native 模式下 `findComponent` 返回 null 即设备端确认不存在(`layoutOk: true`),不再全量 getLayout 确认。`isNativeFindStrategy` 标志区分策略。External 模式保留原 layout 检查(`layoutOk` 语义需要) | ||
| 288 | +- ✅ **waitForTestId 设备端等待**(`ui-automation.ts` + `native-driver-adapter.ts`):native 模式走 `adapter.waitForAndFind()`——设备端 `waitForComponent`(等待在设备侧完成)+ 1 次 `findComponent` 取元素,共 2 WS。替代主机侧 100ms 轮询(8s 超时 ≈ 80 WS)。External 模式保留轮询 | ||
| 289 | +- ✅ **checkDeviceHealth native 分支走 driver ping**(`harmony-system.ts` + `harmony-native-driver.ts` 新增 `ping()`):native 模式 connected 时 1 次 WS ping(~30ms)替代 `echo alive` shell(~500ms)。native driver 未连接直接返回 false(不落回 shell——策略隔离) | ||
| 290 | +- ✅ **性能采集 on-demand 中间层**(`performance-provider.ts` NativePerformanceProvider):缓存未命中时先走 `driver.collectMetrics(pid)`(设备端 on-demand,1 WS ~300ms),仍无数据才 fallback shell hidumper/SP_daemon。降级链变为:推送缓存 → 设备端 on-demand → shell | ||
| 291 | +- ✅ **死代码 synchronizer 移除**(`test-runner.ts`):创建后未使用的 `new Synchronizer()` + `setDriverWaitForIdle`(指向 base hash 实现的死 wiring)已删除 | ||
| 292 | +- ✅ **崩溃恢复统一走 resetApp**(`harmony-system.ts` 新增 `resetApp(level)`):native 模式 driver connected → `driver.resetApp`(设备端 force-stop + aa start + 设备内等待 3s,1 WS);external / fallback → shell 组合(bm clean + stopApp + sleep + launchApp)。三个恢复点接线:①layout-provider 0元素重启 ②stability-test recoverFromCrash ③runner-strategy onUnhealthy。**app 崩溃时 ohosTest 进程通常仍存活(不同进程),WS 可用**——设备端恢复等待不占主机 | ||
| 293 | +- ✅ **通用 shell 透传 action**(`NativeTestDriver.ets` 新增 `shell` action):`shellCmd(delegator, command)` → `{ stdout }`,复用已有 shellCmd helper。`harmony-native-driver.ts` 暴露 `shell(command, timeout)`。NativeStabilityProbe override `runShell`/`runAppStateCommand` 执行原语——**命令不变,执行位置从 host hdc spawn(~400ms)移到设备端 delegator(1 WS ~30ms)**。checkAppAlive(每迭代)、getCrashLogs、forceStopPackage、detectANR、triggerLowMemory、simulateMemoryWarning 全部受益 | ||
| 294 | +- ✅ **navigate 完整 URL 透传**(`NativeTestDriver.ets` navigate action):`cmd.params.url` 优先 → `cmd.value` 含 `://` 直接用 → 否则 `rnoh-test://switch/${caseId}`(向后兼容)。`harmony-native-driver.ts` 新增 `navigateUrl(url)`。NativeNavigationProvider override `deepLink(url)`——回首页导航(test-scene-navigator 5 处 `deepLink('rnoh-test://home')`)从 ~800ms shell → ~30ms WS,fallback 到基类 shell | ||
| 295 | +- ✅ **External 模式 verifyConnection(3000) 不可移除**(`navigation-provider.ts` `ExternalNavigationProvider`):External 导航前必须 `verifyConnection(3000)` 验证 UITest TCP daemon 连接存活。移除后 TCP 连接已断开但代码不知情,后续所有 UITest RPC 调用超时失败,导致 External 模式大面积用例失败。**禁止**为"优化 External 模式性能"而移除此调用 | ||
| 296 | +- ✅ **ExternalLayoutProvider _lastFetchFailure 三处必须重置**(`layout-provider.ts` `ExternalLayoutProvider._getUIElementsInner`):布局获取失败时累计 `_lastFetchFailure`(用于 cooldown/backoff),**成功获取后必须清零**(共 3 处赋值点)。不重置会导致后续所有 `getUIElements` 被 cooldown 跳过,External 模式布局永远返回空。Harmony 和 Android 的 `ExternalLayoutProvider` 均需检查 | ||
| 297 | +- ✅ **截图统一用 snapshot_display**(`media-provider.ts`):native/external 模式截图统一走 `hdc shell snapshot_display`(host 端),**不用 `Driver.screenCap`**(ohosTest 进程沙箱路径 `/data/app/el2/` → `run-as` 拉取路径复杂且权限不稳定)。与 Detox 方案一致(Detox `takeScreenshot` 也用 `snapshot_display`)。**禁止**为 native 模式改用 `screenCap` 再走 `hdc file recv` | ||
| 298 | +- ✅ **watchdog 超时后 executor 并发污染("FPS 9.5Hz 假象"根因)**(`test-runner.ts` runTestCase):超时 race 返回 timeoutResult 后**必须等挂起的 executorPromise 收尾**(grace 10s 未结束则强制等待 ≤60s)再放行下一用例——否则残留 executor 与下一用例并发操作同一设备:WS 单线程命令排队(swipe 42s 卡死)+ 0 值迭代灌入下一用例的 json(perf-article iterations=[58,37,0×8] → 假的 avgFPS 9.5,汇总日志证实真实 60Hz)。配套:`aggregateMetrics` 排除全 0 空转迭代(取消后"无数据"≠"0 帧")。**排查此类"指标异常低"先看 executor 并发,再看采集口径** | ||
| 299 | +- ⏭️ **截图 WS base64 传输**:暂缓(决策),继续用 WS screenCap + `hdc file recv` | ||
| 300 | + | ||
| 301 | +**Stability Probe 策略模式**(`stability-probe.ts`):base 抽象类提供 `runShell(command, timeout)` / `runAppStateCommand(command)` 两个 protected 执行原语,**全部业务逻辑在 base**(pid 缓存交互、crash baseline、重试)。NativeStabilityProbe 仅 override 执行原语(driver.shell 透传 + hdc fallback),ExternalStabilityProbe 空子类。**新增平台 native 能力时优先复用此模式:抽执行原语而非重写业务逻辑** | ||
| 302 | + | ||
| 303 | +**已知设计决策**(非 bug,勿"修复"): | ||
| 304 | + | ||
| 305 | +- ⚠️ **非 TIMEOUT 异常的 double-click 风险**(`input-controller.ts` `shouldFallback`):WS 断连(非 TIMEOUT)时 native click 可能已送达设备但响应丢失,降级到 external 路径可能 double-click。`shouldFallback` 对 TIMEOUT 返回 false(不降级,已防护);非 TIMEOUT 返回 true(宁可 double-click 也不放弃操作)。这是设计权衡:通信层异常无法确定命令是否已执行,Detox 同样接受此风险。**勿改为"全部不降级"**——会导致 WS 闪断时所有操作静默失败 | ||
| 306 | + | ||
| 307 | +- ⚠️ **PortManager 参数顺序**(`port-pool.ts`):`allocate(deviceId, preferredPort, protocol, platform, remotePort)` 共 5 个参数,`setupRport`/`removeRport`/`setupAdbReverse`/`removeAdbReverse` 的 localPort 与 remotePort **位置不同**(setupRport: `local, remote`;setupAdbReverse: `remote, local`)。调错顺序导致端口转发无效,ohosTest 或 instrumentation 永远连不上。修改时对照源码签名,不要靠直觉猜顺序 | ||
| 308 | +| ArkTS 编译错误 | any/unknown/未类型化字面量 | 参见 cptf-troubleshoot ArkTS 章节 | | ||
| 309 | + | ||
| 310 | +## HarmonyOS 设备端已知限制 | ||
| 311 | + | ||
| 312 | +| 限制 | 根因 | 影响 | | ||
| 313 | +|------|------|------| | ||
| 314 | +| **`executeShellCommand` 阻塞 JS 线程** | ohosTest 进程内 `delegator.executeShellCommand()` 同步阻塞 JS 线程(~3-4s/次) | WS 命令(click/swipe)处理停滞;**不适合持续采样**(如 FPS 轮询),只适合低频 on-demand 调用 | | ||
| 315 | +| **FPS 60Hz 限制(AAMS 连接)** | ohosTest 进程持有 AAMS 连接,影响系统刷新率策略,FPS 被限制在 60Hz | **设备端采集 FPS 会引入 60Hz 上限**;host 端 `SP_daemon` 采集不受此限制(最高 120Hz)。回退到 host 端采集后 FPS 数据恢复正常 | | ||
| 316 | +| **`hidumper -s RenderService -a "fpsCount"` 不完整** | RenderService 异步服务,`executeShellCommand` 可能不等响应即返回 | 只返回标题(~157 字符),无 `Refresh Rate` 数据。改用 `hidumper hitchs`(完整)或继续 host 端采集 | | ||
| 317 | +| **`SP_daemon` exitCode=-1** | ohosTest 进程沙箱权限或 `$PATH` 不含 SP_daemon | 设备端无法直接运行 SP_daemon。性能采集降级链:推送缓存 → 设备端 on-demand `collectMetrics`(hidumper pid/mem/cpu)→ host shell SP_daemon | | ||
| 318 | +| **ArkUI 无障碍属性不进组件树**(2026-08-26 穿刺实锤) | `accessibilityText`/`accessibilityDescription`/`accessibilityGroup` 设计上只进无障碍树(AccessibilityElement,读屏消费),uitest 三通道(dumpLayout 默认/`-i`/`-a`、Component API)均不导出——**平台分层设计,非 RNOH 缺陷**(RNOH 已正确调用 `instance.accessibilityText()`,RNViewBase.ets:124 源码确认) | harmony 无法自动验证 accessibilityLabel/Hint/accessible/role 声明值;`getDescription()` 只读 ArkUI description 属性(RNOH 不往这写,值恒空)。可用替代:`isChecked/isCheckable`(Switch 类已接,实测通过)。expect 层用 `platform: ['android']` 门控(Android contentDesc 可读,端到端实证)。官方唯一全量通道:AccessibilityExtensionAbility(需独立 extension 进程 + 用户开启,框架不可透明复用) | | ||
| 319 | + | ||
| 320 | +## HarmonyOS UiTest API 签名陷阱(2026-08-24 实测,SDK 权威验证) | ||
| 321 | + | ||
| 322 | +以 `@ohos.UiTest.d.ts`(`/path/to/sdk/default/openharmony/ets/api/`)为准,**不要凭记忆写 API 调用**: | ||
| 323 | + | ||
| 324 | +### 陷阱 1:`dumpLayout(savePath, displayId?): Promise<boolean>`(不是无参返回 JSON) | ||
| 325 | + | ||
| 326 | +- **错误写法**(曾导致 dumpLayout 恒返回空):`await driver.dumpLayout()` 无参调用 → 参数错误 → catch 静默返回空 | ||
| 327 | +- **正确用法**:`const ok = await driver.dumpLayout(sandboxPath)` → `fileIo.readTextSync(sandboxPath)` → `fileIo.unlinkSync(sandboxPath)` | ||
| 328 | +- 产物格式 = external 模式 CLI `uitest dumpLayout` 的 JSON(attributes 树),host 端 `traverseLayoutTree` 现成解析 | ||
| 329 | + | ||
| 330 | +### 陷阱 2:`screenCap(savePath)` / `dumpLayout(savePath)` 要求 **app 沙箱路径** | ||
| 331 | + | ||
| 332 | +- 传 `/data/local/tmp/...` → `Invalid file path` 异常(SDK 文档明确 "must be in the application sandbox directory") | ||
| 333 | +- **沙箱路径获取**:`delegator.getAppContext().tempDir`(如 `/data/storage/el2/base/temp`) | ||
| 334 | +- **host 拉取障碍**:`hdc file recv` **无法访问沙箱逻辑路径**(`/data/storage/el2/...`)——必须转换为**物理路径** `/data/app/el2/100/base/<bundle>/temp/`(实测 hdc 可读) | ||
| 335 | +- **转换规则**(`UiDriverWrapper.publicPathFor()`):`sandboxPath.split('/').pop()` → 拼 `/data/app/el2/100/base/${bundleName}/temp/` | ||
| 336 | + | ||
| 337 | +### 陷阱 3:`ON.id()` 不支持 pattern 参数(静默忽略第二参数) | ||
| 338 | + | ||
| 339 | +- SDK 签名:`id(id: string): On`(**仅单参数**);`text(txt: string, pattern?: MatchPattern): On`(支持 pattern) | ||
| 340 | +- **错误写法**(曾导致 `'.*'` 正则枚举恒返回 0):`ON.id('.*', MatchPattern.REG_EXP)` 第二参数被忽略 → 精确匹配 `'.*'` → 0 结果,**无任何报错** | ||
| 341 | +- **正则枚举替代方案**:`ON.text` 正则(仅 Text 组件)或 `ON.type` 精确匹配循环(探针实测 Text/Scroll/Image 可用) | ||
| 342 | +- `MatchPattern` 枚举:`EQUALS=0, CONTAINS=1, STARTS_WITH=2, ENDS_WITH=3, REG_EXP=4` | ||
| 343 | + | ||
| 344 | +### 陷阱 4:WS selector 的 `pattern` 字段在 dispatch 层被丢弃 | ||
| 345 | + | ||
| 346 | +- 设备端 `dispatch()` 只拷贝 `kind`/`value`,`spec['pattern']` 恒 undefined → 正则能力从未生效 | ||
| 347 | +- 修复:dispatch 补 `spec['pattern'] = sel['pattern']` 转发(findComponent/findComponents 分支) | ||
| 348 | + | ||
| 349 | +### 沙箱路径辅助(UiDriverWrapper 已实现) | ||
| 350 | + | ||
| 351 | +```typescript | ||
| 352 | +resolveSandboxDir(delegator, bundleName): void // 初始化 sandboxDir + sandboxPublicDir | ||
| 353 | +sandboxPathFor(requested): string // /data/local/tmp/x.jpeg → tempDir/x.jpeg(写入用) | ||
| 354 | +publicPathFor(sandboxPath): string // tempDir/x.jpeg → /data/app/el2/.../temp/x.jpeg(返回 host 用) | ||
| 355 | +``` | ||
| 356 | + | ||
| 357 | +### screenCap 性能实测(Mate 70 Pro,2026-08-24) | ||
| 358 | + | ||
| 359 | +| 方式 | 平均耗时(含 pull + 清理) | | ||
| 360 | +|------|--------------------------| | ||
| 361 | +| Driver.screenCap via WS(沙箱写入 + hdc file recv 物理路径) | ~3156ms | | ||
| 362 | +| snapshot_display shell 合并命令(当前 screenshotBatch 方案) | ~2328ms | | ||
| 363 | +| 单次 Driver.screenCap via WS(不含 pull) | ~247ms | | ||
| 364 | + | ||
| 365 | +**结论**:单次调用 Driver.screenCap 快(247ms),但含 pull 的完整链路反而慢于 shell 批量(每次 hdc file recv spawn 开销 ~2s)。screenshotBatch 保持 shell 合并命令方案更优。 | ||
| 366 | + | ||
| 367 | +--- | ||
| 368 | + | ||
| 369 | +# Android Native Driver(v0.3.x 已实现,实测通过) | ||
| 370 | + | ||
| 371 | +## Architecture(与 Harmony 镜像) | ||
| 372 | + | ||
| 373 | +``` | ||
| 374 | +Host (CPTF Node.js) | ||
| 375 | + └── AndroidNativeDriver (WS Server, PortManager 分配端口, maxPayload 256MB) | ||
| 376 | + │ adb reverse tcp:<port> tcp:<port> ← PortManager.setupAdbReverse | ||
| 377 | + ▼ | ||
| 378 | + 设备端 instrumentation 进程(am instrument 启动) | ||
| 379 | + ├── DeviceDriver.java(WS Client 手写 RFC6455 + dispatch) | ||
| 380 | + ├── UiDevice + UiObject2(Component-direct: By.res(testID)) | ||
| 381 | + └── Timer 周期采集(gfxinfo 帧差分 fps + meminfo PSS + top CPU → WS push) | ||
| 382 | + | ||
| 383 | +入口: adb shell am instrument -w -r -e class com.cptf.test.DeviceDriver | ||
| 384 | + -e host 127.0.0.1 -e port <port> <pkg>.test/androidx.test.runner.AndroidJUnitRunner | ||
| 385 | +``` | ||
| 386 | + | ||
| 387 | +## 与 Harmony 的关键差异(易踩坑,全部实测验证) | ||
| 388 | + | ||
| 389 | +| 项 | Harmony | Android | 根因 | | ||
| 390 | +|---|---|---|---| | ||
| 391 | +| 元素定位 | `ON.id(testID)` | `UiDevice.findObject(By.res(testID))` + `By.desc` fallback | RN testID → Android resource-id | | ||
| 392 | +| dumpLayout 格式 | **JSON 树** | **自实现 AccessibilityNodeInfo XML(全窗口+hint)** → host 用 `parseUILayout` 解析,**不要 JSON.parse** | UiDevice.dumpWindowHierarchy 是 compressed 且丢 hint/丢窗口 | | ||
| 393 | +| Component-direct API | `comp.click()/inputText()/scrollToTop()` | `shellInput("input tap/swipe ...")`(⚠️ 勿用 UiObject2 手势,见坑6) | ets 有 Component 对象,Java 是 UiObject2 | | ||
| 394 | +| waitForComponent | driver.waitForComponent(on) | `uiDevice.wait(Until.hasObject(By.res(t)), timeout)` | — | | ||
| 395 | +| 端口穿透 | hdc rport | **adb reverse**(设备以 127.0.0.1 访问 host) | — | | ||
| 396 | +| 进程模型 | ohosTest(aa test,startAbility 启动 app) | instrumentation(am instrument,**须 DeviceDriver.launchTargetApp() 启动 app**) | UiAutomation 服务全系统单例 | | ||
| 397 | + | ||
| 398 | +## Android 专属坑(实测全部踩过) | ||
| 399 | + | ||
| 400 | +1. **UiAutomation 冲突**:ATX(com.wetest.uia2)占用 UiAutomation 服务时,DeviceDriver 的 `UiDevice.getInstance()` 抛 `IllegalStateException: UiAutomationService already registered`。**修复**:launchInstrumentation 前 kill com.wetest.uia2;native 模式成功后 `u2.destroy()` 防 ATX 复活抢占。**v2 修复**:`AndroidSystem._doInitU2()` 在 `driverMode==='native'` 时直接跳过 ATX 启动(启动也必然失败,且失败进程会干扰 input 注入)。 | ||
| 401 | +2. **手写 WS client 必须 mask**:RFC 6455 client→server 帧必须置 mask 位 + mask payload,否则 Node ws 报 `Invalid WebSocket frame: MASK must be set`。帧编码三分支(≤125 / ≤65535 用 126+16bit / else 127+64bit)都要带 0x80 mask 位。 | ||
| 402 | +3. **截图不要走 /sdcard**:instrumentation 进程写 /sdcard 受 scoped storage 限制且旧版 takeScreenshot 静默失败。**用 `UiAutomation.takeScreenshot()` → Bitmap → 写 `getExternalFilesDir(null)`(app 外部私有目录)→ host adb pull**。走 WS base64 回传大帧会触发 ws `Max payload size exceeded`(勿用)。 | ||
| 403 | +4. **dumpWindowHierarchy 无 boolean 重载**:那是 ATX 的 API。UiDevice 只有 `(String)/(File)/(OutputStream)` 重载,用 `dumpWindowHierarchy(ByteArrayOutputStream)` 取字符串。 | ||
| 404 | +5. **nohup screenrecord 在 Android sh 不可靠**:adb shell 会话退出连带杀进程。录屏改 **host 托管**:`spawn(adb, ['shell', 'screenrecord ...'], {detached:true})` 由 host 持有会话;启动前 `which screenrecord` 检查,缺失直接跳过。 | ||
| 405 | +6. **⛔ UiObject2/UiAutomation 手势注入在部分 ROM 上恒定 5s/事件(华为 ALN-AL00 Android 12 实测)**:`GestureController: Gestures took longer than expected (10833ms)` / `injectInputEvent` 每次 5s(async 模式同样阻塞)。根因链:ROM 的 `Choreographer.doFrame → AnimationUtils.unlockAnimationClock` 与 UiAutomation 注入路径的动画时钟锁竞争 → app 主线程消费 input 事件延迟 5s → InputDispatcher ANR 计时(`Waited 5001ms for MotionEvent(UP)`)→ clickComponent WS round-trip 恒定 ~10.1s。**修复**:DeviceDriver 全部手势(tap/longClick/swipe/scroll/scrollSearch)改走 `Runtime.exec("input tap|swipe ...")`(shell input 命令,实测 74ms)。**禁止**在 DeviceDriver 内使用 `uiDevice.click/swipe`、`UiObject2.click/scroll`、`uiAutomation.injectInputEvent`(含 runOnMainSync 包装——主线程注入直接死锁:主线程等注入返回,注入等主线程消费事件)。 | ||
| 406 | +7. **设备端 app 启动**:`am instrument` 只起进程不起 Activity(Harmony 是 ohosTest startAbility 启动)。DeviceDriver.runDeviceDriver 在 WS connect 后必须 `getLaunchIntentForPackage(pkg) + FLAG_ACTIVITY_NEW_TASK|CLEAR_TOP` 主动启动被测 app(`launchTargetApp()`),否则 dump 全是桌面。 | ||
| 407 | +8. **dumpLayout 必须 AccessibilityNodeInfo 树自实现(getWindows() 全窗口)**:`UiDevice.dumpWindowHierarchy` 是 compressed XML——**剔除 hint 属性**(placeholder 断言失败)且只含部分窗口。自实现要点:`getUiAutomation().getWindows()` 遍历每窗口 root 递归序列化(`getViewIdResourceName()`=testID、API26+ `getHintText()`=placeholder、叶子节点自闭合 `<node .../>` 容器节点 `</node>`——host 的 parseUILayout 栈式解析依赖此结构)。**RN Modal 是独立窗口**——只 dump active window 会丢主窗口内容(Modal 打开时主窗口元素"不存在")。 | ||
| 408 | +9. **findComponent 的 placeholder 补齐**:UiObject2 无 hint API。`serializeState` 后用 `findHintByResId(resId)`(accessibility 树按 viewIdResourceName 精确匹配,返回 hintText)补充 `placeholder` 字段。`'*'` 查询跳过(防 O(N²))。host 侧 `NativeFindStrategy.supplementPlaceholderFromLayout` 兜底(TextInput/EditText 类 + placeholder 未定义时从布局缓存合并——Harmony 复用此路径)。 | ||
| 409 | +10. **instrumentation 冷启动 dexopt 首次 >30s**:连接超时必须 ≥60s(首次 am instrument 后 AOT/dex 校验),否则首轮全超时、二轮正常(假象不稳定)。 | ||
| 410 | + | ||
| 411 | +## Android L2/L3/L4(镜像 Harmony 实现) | ||
| 412 | + | ||
| 413 | +- **L3 sendCommand 重试**:`isRetryableError` 重试 1 次(timeout 类等 500ms,其他 200ms) | ||
| 414 | +- **L2 tryReconnect**:3 次 × 指数退避,每次重新 am instrument + waitWSConnection(5s);`reconnectCount` 计数 | ||
| 415 | +- **L4 recoverFromCrash(bundleId)**:`adb shell am force-stop` → `monkey -p <pkg> LAUNCHER 1` → sleep 3s → tryReconnect | ||
| 416 | +- **L1 降级**(test-runner android 分支):connect 成功 → remount Native mixins + destroy u2;失败且 auto → 切 external mixin + `u2.reconnect()`。**DriverModeArbiter 仅 harmony 用**(android 分支内联同语义逻辑) | ||
| 417 | + | ||
| 418 | +## connect() 失败必须清理(两平台同规则) | ||
| 419 | + | ||
| 420 | +connect 超时/失败路径必须:关 WS server + `PortManager.release(port)` + 清 `_actualPort`,否则**进程内单例 Map 记录泄漏 → 重试时端口漂移**(18081→18082→18083,漂移端口的 adb reverse 未建 → instrumentation 永远连不上,表象为"connect timeout on 18082")。 | ||
| 421 | + | ||
| 422 | +## Android native 实测结论(2026-08 全量对比) | ||
| 423 | + | ||
| 424 | +- 功能判例与 external **100% 一致**(109 采样用例 0 分歧),平均用例时长 **2.6s vs external 4.4s(快 41%)** | ||
| 425 | +- 渲染基准 renderTime/unmountTime 偏差 <8%;FPS 采集比 external 更真实(无 dump 干扰) | ||
| 426 | +- ⚠️ **高交互场景(持续动画 perf-animations/perf-balls)有卡死风险**(超时无返回)——嫌疑 DeviceDriver 单线程 receive 循环被采集 Timer 抢占。**动画类用例暂用 external**,待命令通道并发修复 | ||
| 427 | +- 全程 0 fallback / 0 reconnects(连接稳定) | ||
| 428 | + | ||
| 429 | +## Native 模式完全跳过 Daemon(v0.3.x 完整隔离链路) | ||
| 430 | + | ||
| 431 | +native 模式下,**所有外部 daemon 必须跳过**,否则与设备端 ohosTest / instrumentation 进程冲突(UiAutomation 全系统单例)。 | ||
| 432 | + | ||
| 433 | +### 跳过项清单 | ||
| 434 | + | ||
| 435 | +| 平台 | Daemon/Agent | 跳过位置 | 跳过原因 | | ||
| 436 | +|------|--------------|---------|---------| | ||
| 437 | +| 鸿蒙 | uitest daemon (`app_process /com.ohos.testkit.teskit.teskit_service`) | `HarmonySystem.warmup()` / `initUitest()` / `resetUitest()` native 分支早返 | ohosTest 进程独占 UiAutomation 服务,uitest daemon 启动会抛 `UiAutomationService already registered` | | ||
| 438 | +| Android | ATX/u2 agent (`com.wetest.uia2.Main`) | `AndroidSystem._doInitU2()` native 分支早返;`AndroidSystem.warmup()` 走 `initNativeDriver()` | 同上 UiAutomation 单例冲突 | | ||
| 439 | +| 鸿蒙 | `HarmonyLayoutProvider` (Uitest dump + shell) | 构造时定 native → `HarmonyNativeLayoutProvider` 走 `driver.dumpLayout()` | 完全避免 Uitest dump | | ||
| 440 | +| Android | `AndroidLayoutProvider` (U2 dump + uiautomator dump) | 构造时定 native → `AndroidNativeLayoutProvider` 走 `driver.dumpLayout()` | 完全避免 U2/uiautomator dump | | ||
| 441 | +| iOS | WDA(XCTest)| iOS 无 native 模式 | 仅 external 模式 | | ||
| 442 | + | ||
| 443 | +### 完整 native 链路(按调用顺序) | ||
| 444 | + | ||
| 445 | +``` | ||
| 446 | +TestRunner.runTestCase() | ||
| 447 | + ├─ TestRunner.executeTestCase Native 模式跳过 stopApp/launchApp/warmup(ohosTest/am instrument 已启 app) | ||
| 448 | + ├─ TestRunner.runCaseQueue Native 模式 failed → nativeDriver.recoverFromCrash(bundleId)(L4) | ||
| 449 | + │ ├─ HarmonyNativeDriver: stopApp → launchApp → tryReconnect | ||
| 450 | + │ └─ AndroidNativeDriver: stopApp → launchApp → tryReconnect | ||
| 451 | + └─ TestRunner.runCaseQueue result.effectiveDriverMode = driverInfo.effective(写入报告) | ||
| 452 | + | ||
| 453 | +getUIElements() 调用链: | ||
| 454 | + ├─ AndroidSystem.layout → AndroidNativeLayoutProvider(构造定) | ||
| 455 | + │ └─ driver.dumpLayout()(facade 优先,鸿蒙走 ON.dump) | ||
| 456 | + └─ HarmonySystem.layout → HarmonyNativeLayoutProvider(构造定) | ||
| 457 | + └─ driver.getLayout() / facade.getLayout() | ||
| 458 | + | ||
| 459 | +getElementByTestId() 调用链: | ||
| 460 | + ├─ NativeFindStrategy.findByTestId() → facade.findComponent(1 WS,native 路径) | ||
| 461 | + └─ catch fallback → getLayout() 过滤(仅在 facade 失败时) | ||
| 462 | +``` | ||
| 463 | + | ||
| 464 | +### 测试覆盖(验证 native 隔离生效) | ||
| 465 | + | ||
| 466 | +```typescript | ||
| 467 | +// 类型 / 单测检查:native 模式不应触发 UitestTransport.connect | ||
| 468 | +expect(HarmonySystem.warmup).toBeNativeSkipUitestInit(); | ||
| 469 | +expect(AndroidSystem._doInitU2).toBeNativeSkipU2Connect(); | ||
| 470 | +expect(AndroidNativeLayoutProvider.getUIElements).toUseNativeDriverOnly(); | ||
| 471 | + | ||
| 472 | +// 设备端实际验证:native 模式跑用例后 dump 设备进程列表 | ||
| 473 | +expect(adb.shell.ps).not.toContain('com.ohos.testkit.teskit.teskit_service'); // 鸿蒙 | ||
| 474 | +expect(adb.shell.ps).not.toContain('com.wetest.uia2.Main'); // Android | ||
| 475 | +``` | ||
| 476 | + | ||
| 477 | +### 何时启动 Daemon(不是 native 模式时) | ||
| 478 | + | ||
| 479 | +| 平台 | external 模式 | 自动 daemon 启动 | | ||
| 480 | +|------|---------------|------------------| | ||
| 481 | +| 鸿蒙 | UitestRunner.init → ensureAgentRunning | uitest daemon (com.ohos.testkit.teskit.teskit_service) 启动 | | ||
| 482 | +| Android | AndroidU2Client.connect | u2 agent (com.wetest.uia2.Main) 启动 | | ||
| 483 | +| iOS | IosWdaClient.connect | WDA XCTest 启动(首次或定期检测) | | ||
| 484 | + | ||
| 485 | +### 扩展新平台(future) | ||
| 486 | + | ||
| 487 | +新平台 native driver 接入时,必须: | ||
| 488 | +1. 在 `BaseSystem.warmup()` override native 分支早返(不调 daemon) | ||
| 489 | +2. 在 `_doInitXxx()` 类似方法 native 分支早返(或新建 native-only 路径) | ||
| 490 | +3. 创建 `XxxNativeLayoutProvider` 走 `nativeDriver.dumpLayout()` 而非外部 dump | ||
| 491 | +4. 在 executeTestCase 的 Native 分支类似位置检查 `nativeDriver` 是否 connected 才 fallback | ||
| @@ -0,0 +1,431 @@ | |||
| 1 | +--- | ||
| 2 | +name: cptf-optimization-verification | ||
| 3 | +description: Guide for evaluating code optimization proposals from static analysis tools, code reviews, or performance audits. Use when receiving optimization suggestions to avoid false positives. Covers verification-first workflow, Node.js concurrency misconceptions, common false positive patterns, and cross-validation techniques. Triggers: "optimization proposal", "static analysis report", "code review suggestions", "performance audit", "concurrency safety", "memory leak", "race condition", "code smell". | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# CPTF 优化建议验证指南 | ||
| 7 | + | ||
| 8 | +本 skill 总结了从静态分析报告、代码审查、性能审计中评估优化建议的实战经验。**核心原则:先验证,后修复**。 | ||
| 9 | + | ||
| 10 | +## 关键教训:静态分析工具的典型误报模式 | ||
| 11 | + | ||
| 12 | +### 1. 行号错位(Line Number Drift) | ||
| 13 | + | ||
| 14 | +**现象**:报告指向 `performance-collector.ts:45-52` 的 EventBus 泄漏,实际行号指向常量定义。 | ||
| 15 | + | ||
| 16 | +**根因**:代码频繁重构,行号持续漂移。静态工具缓存的行号与实际代码不匹配。 | ||
| 17 | + | ||
| 18 | +**验证方法**: | ||
| 19 | +```bash | ||
| 20 | +# 直接查看报告行号的内容 | ||
| 21 | +sed -n '45,52p' src/capability/measurement/performance-collector.ts | ||
| 22 | + | ||
| 23 | +# 搜索实际位置 | ||
| 24 | +grep -n "EventEmitter\|EventBus\|\.on(" src/capability/measurement/performance-collector.ts | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +**决策规则**:行号内容与报告描述不符 → **忽略该行号**,用全文搜索定位真实位置。 | ||
| 28 | + | ||
| 29 | +### 2. 文件存在性错误(File Existence Error) | ||
| 30 | + | ||
| 31 | +**现象**:报告指向 `test-execution-loop.ts:189-205` 的竞态条件,但文件不存在。 | ||
| 32 | + | ||
| 33 | +**根因**:静态工具扫描的是旧版本代码或分支。 | ||
| 34 | + | ||
| 35 | +**验证方法**: | ||
| 36 | +```bash | ||
| 37 | +# 检查文件是否存在 | ||
| 38 | +ls -la src/orchestration/test-execution-loop.ts | ||
| 39 | + | ||
| 40 | +# 查找相关文件 | ||
| 41 | +find src -name "*execution*" -type f | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +**决策规则**:文件不存在 → **报告完全无效**,可能指向已删除的代码。 | ||
| 45 | + | ||
| 46 | +### 3. Node.js 单线程并发误判(Concurrency Misconceptions) | ||
| 47 | + | ||
| 48 | +**现象**:报告说 `NavigationCoordinator` 的 12 个静态 Map 需要加锁保护。 | ||
| 49 | + | ||
| 50 | +**根因**: | ||
| 51 | +- 混淆了"并发竞态"(多线程同时访问)和"语义竞态"(异步任务读到旧值) | ||
| 52 | +- Node.js 的单线程事件循环模型下,同步 Map 操作原子性由 JavaScript 引擎保证 | ||
| 53 | +- 工具无法理解 `Promise.race` + `maxConcurrency` 的串行化设计 | ||
| 54 | + | ||
| 55 | +**验证方法**: | ||
| 56 | +1. 确认代码运行环境(Node.js / Browser / Worker) | ||
| 57 | +2. 检查是否真的有多线程(Worker Threads / Child Process) | ||
| 58 | +3. 分析 key 设计是否包含设备隔离(如 `${platform}:${deviceId}`) | ||
| 59 | +4. 验证并发控制机制(队列、串行化、降级策略) | ||
| 60 | + | ||
| 61 | +**决策规则**: | ||
| 62 | +- Node.js 主线程 + 同步操作 → **无需加锁** | ||
| 63 | +- Worker Threads 共享内存 → 需要 Atomics / MessageChannel | ||
| 64 | +- 跨进程协调 → 需要文件锁 / Redis 分布式锁 | ||
| 65 | + | ||
| 66 | +### 4. "设计模式"误判为"缺陷"(Design vs Defect) | ||
| 67 | + | ||
| 68 | +**现象**: | ||
| 69 | +- 113 处 `try { ... } catch {}` 被报告为"异常吞没" | ||
| 70 | +- 85% 的"魔法数字"实际是单位转换或配置默认值 | ||
| 71 | + | ||
| 72 | +**根因**:静态工具不理解业务上下文: | ||
| 73 | +- `catch {}` 在资源清理场景是合理的 fire-and-forget 模式 | ||
| 74 | +- `1000` 作为毫秒→秒转换系数不是魔法数字 | ||
| 75 | +- 配置默认值(如 `DEFAULT_NAVIGATION_CONFIG`)应该显式列出 | ||
| 76 | + | ||
| 77 | +**验证方法**: | ||
| 78 | +```bash | ||
| 79 | +# 统计并分类所有 catch {} | ||
| 80 | +grep -rn "catch {}" src --include="*.ts" | wc -l # 113 处 | ||
| 81 | + | ||
| 82 | +# 检查是否已有命名常量 | ||
| 83 | +grep -rn "const.*=.*1000" src | head -20 | ||
| 84 | + | ||
| 85 | +# 检查配置项定义 | ||
| 86 | +grep -A 5 "DEFAULT_.*CONFIG" src/orchestration/core/decorators.ts | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +**决策规则**: | ||
| 90 | +- 资源清理 / 可选 API 调用 / 探测式操作 → `catch {}` **合理** | ||
| 91 | +- 单位转换 / 命名常量 / 配置默认值 → **不是魔法数字** | ||
| 92 | +- 业务逻辑中的裸数字 → 可能需要提取为常量 | ||
| 93 | + | ||
| 94 | +### 5. 工具配置缺失导致的误报(Tool Configuration Gaps) | ||
| 95 | + | ||
| 96 | +**现象**: | ||
| 97 | +- ESLint 报告 12 处"未使用导入",实跑结果 0 个 | ||
| 98 | +- TypeScript 报告类型错误,实际编译通过 | ||
| 99 | + | ||
| 100 | +**根因**:工具配置不完整或未启用关键规则。 | ||
| 101 | + | ||
| 102 | +**验证方法**: | ||
| 103 | +```bash | ||
| 104 | +# 实际运行 ESLint | ||
| 105 | +pnpm lint 2>&1 | tail -20 | ||
| 106 | + | ||
| 107 | +# 检查 ESLint 配置 | ||
| 108 | +cat .eslintrc*.js 2>/dev/null || cat eslint.config.* 2>/dev/null | ||
| 109 | + | ||
| 110 | +# 实跑 TypeScript | ||
| 111 | +pnpm typecheck 2>&1 | tail -20 | ||
| 112 | +``` | ||
| 113 | + | ||
| 114 | +**决策规则**:实跑工具结果与报告不符 → **以实跑为准**。 | ||
| 115 | + | ||
| 116 | +### 6. 过度优化的诱惑(Over-Optimization Trap) | ||
| 117 | + | ||
| 118 | +**现象**:建议为每个 `catch {}` 添加 `logger.debug(...)`,增加 113 行噪音代码。 | ||
| 119 | + | ||
| 120 | +**根因**:追求"代码整洁度指标"而非实际收益。 | ||
| 121 | + | ||
| 122 | +**决策规则**: | ||
| 123 | +- 修改后代码行数增加 > 收益 → **拒绝** | ||
| 124 | +- 违反项目既有风格(如 CPTF 的"简洁输出"原则)→ **拒绝** | ||
| 125 | +- 无运行时行为改进 → **拒绝** | ||
| 126 | + | ||
| 127 | +## 验证优先工作流(Verification-First Workflow) | ||
| 128 | + | ||
| 129 | +收到优化建议后,按以下顺序验证: | ||
| 130 | + | ||
| 131 | +### Step 1: 行号验证(30 秒) | ||
| 132 | + | ||
| 133 | +```bash | ||
| 134 | +# 查看报告行号的实际内容 | ||
| 135 | +sed -n '<start>,<end>p' <file> | ||
| 136 | + | ||
| 137 | +# 搜索报告提到的关键符号 | ||
| 138 | +grep -n '<symbol_name>' <file> | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +**通过标准**:行号内容与报告描述匹配。 | ||
| 142 | + | ||
| 143 | +### Step 2: 文件存在性验证(10 秒) | ||
| 144 | + | ||
| 145 | +```bash | ||
| 146 | +ls -la <file_path> | ||
| 147 | +find src -name "<file_name>" -type f | ||
| 148 | +``` | ||
| 149 | + | ||
| 150 | +**通过标准**:文件存在且路径正确。 | ||
| 151 | + | ||
| 152 | +### Step 3: 业务上下文验证(2 分钟) | ||
| 153 | + | ||
| 154 | +**并发安全类**: | ||
| 155 | +1. 确认运行环境(Node.js 主线程 vs Worker vs 子进程) | ||
| 156 | +2. 检查 key 设计是否包含隔离维度(设备 ID / 平台) | ||
| 157 | +3. 分析并发控制机制(队列长度、串行化、降级策略) | ||
| 158 | +4. 阅读 `CONCURRENCY-SAFETY.md` 了解项目并发模型 | ||
| 159 | + | ||
| 160 | +**资源泄漏类**: | ||
| 161 | +1. 全文搜索资源类型(`EventEmitter` / `fs.readFile` / `setTimeout`) | ||
| 162 | +2. 检查是否已有清理逻辑(`destroy()` / `cleanup()` / `removeAllListeners()`) | ||
| 163 | +3. 验证调用链是否调用清理方法 | ||
| 164 | + | ||
| 165 | +**类型安全类**: | ||
| 166 | +1. 统计 `any` 类型总数 | ||
| 167 | +2. 分类:动态导入 vs 配置深拷贝 vs API 兼容层 | ||
| 168 | +3. 评估改进成本 vs 收益 | ||
| 169 | + | ||
| 170 | +### Step 4: 实跑验证(5 分钟) | ||
| 171 | + | ||
| 172 | +```bash | ||
| 173 | +# TypeScript 类型检查 | ||
| 174 | +pnpm typecheck 2>&1 | tail -20 | ||
| 175 | + | ||
| 176 | +# ESLint 检查 | ||
| 177 | +pnpm lint 2>&1 | tail -20 | ||
| 178 | + | ||
| 179 | +# 单元测试 | ||
| 180 | +pnpm test 2>&1 | tail -20 | ||
| 181 | +``` | ||
| 182 | + | ||
| 183 | +**通过标准**: | ||
| 184 | +- `tsc --noEmit` 无错误 | ||
| 185 | +- ESLint 0 warnings(或仅与报告相关的 warnings) | ||
| 186 | +- 所有测试通过 | ||
| 187 | + | ||
| 188 | +### Step 5: 成本收益评估(1 分钟) | ||
| 189 | + | ||
| 190 | +| 维度 | 问题 | 决策 | | ||
| 191 | +|------|------|------| | ||
| 192 | +| 代码行数 | 修改后增加多少行? | 增加 > 50 行需慎重 | | ||
| 193 | +| 风格一致性 | 是否违反项目风格? | 违反则拒绝 | | ||
| 194 | +| 运行时收益 | 能否改进性能/正确性? | 无收益则拒绝 | | ||
| 195 | +| 维护成本 | 后续修改是否更复杂? | 显著增加则拒绝 | | ||
| 196 | + | ||
| 197 | +## 常见误报案例库 | ||
| 198 | + | ||
| 199 | +### Case 1: NavigationCoordinator 并发安全 | ||
| 200 | + | ||
| 201 | +**报告**:`orchestration/core/base-test.ts:25-35` 的 12 个静态 Map 需要加锁。 | ||
| 202 | + | ||
| 203 | +**验证**: | ||
| 204 | +1. ✅ 行号正确(指向 `NavigationCoordinator` 声明) | ||
| 205 | +2. ✅ 文件存在 | ||
| 206 | +3. ❌ 业务上下文:Node.js 单线程 + Map 键包含 `${platform}:${deviceId}` 隔离 | ||
| 207 | +4. ✅ 已有防御:`maxConcurrency` 默认 1 + `test-runner.ts:655-659` 并发模式禁用优化 | ||
| 208 | + | ||
| 209 | +**结论**:❌ 误报。详见 `docs/architecture/CONCURRENCY-SAFETY.md`。 | ||
| 210 | + | ||
| 211 | +### Case 2: EventBus 内存泄漏 | ||
| 212 | + | ||
| 213 | +**报告**:`capability/measurement/performance-collector.ts:45-52` 的全局 EventBus 未清理。 | ||
| 214 | + | ||
| 215 | +**验证**: | ||
| 216 | +1. ❌ 行号错误(实际指向 `SOURCE_CODE` 常量映射) | ||
| 217 | +2. ❌ 全文搜索 `EventEmitter | EventBus | .on(` 无匹配 | ||
| 218 | +3. ✅ 文件存在 | ||
| 219 | + | ||
| 220 | +**结论**:❌ 误报。行号漂移 + 符号不存在。 | ||
| 221 | + | ||
| 222 | +### Case 3: test-execution-loop 竞态条件 | ||
| 223 | + | ||
| 224 | +**报告**:`orchestration/test-execution-loop.ts:189-205` 的 Promise.race 竞态。 | ||
| 225 | + | ||
| 226 | +**验证**: | ||
| 227 | +1. ❌ 文件不存在(`No such file or directory`) | ||
| 228 | +2. ✅ 竞态逻辑实际在 `test-runner.ts:1065-1109` | ||
| 229 | +3. ✅ 该逻辑已正确实现(watchdog 清理 + null 检查 + try-catch) | ||
| 230 | + | ||
| 231 | +**结论**:❌ 误报。文件路径错误 + 逻辑已正确。 | ||
| 232 | + | ||
| 233 | +### Case 4: android-u2-client timeout 僵尸进程 | ||
| 234 | + | ||
| 235 | +**报告**:`system/android/android-u2-client.ts:234` 的 `timeout: 0` 导致僵尸进程。 | ||
| 236 | + | ||
| 237 | +**验证**: | ||
| 238 | +1. ❌ 行号错误(第 234 行是 `await this.sleep(1000);`) | ||
| 239 | +2. ❌ 实际 timeout 是 `600000ms`(10 分钟) | ||
| 240 | +3. ✅ 文件存在 | ||
| 241 | + | ||
| 242 | +**结论**:❌ 误报。行号漂移 + 值已修复。 | ||
| 243 | + | ||
| 244 | +### Case 5: 代码风格问题(4 项) | ||
| 245 | + | ||
| 246 | +**报告**: | ||
| 247 | +- 空 catch 块 19 处 | ||
| 248 | +- 魔法数字 23 处 | ||
| 249 | +- 重复 testID 生成 7 处 | ||
| 250 | +- 未使用导入 12 处 | ||
| 251 | + | ||
| 252 | +**验证**: | ||
| 253 | +1. ✅ 空 catch 实际 113 处,全部符合 fire-and-forget 设计 | ||
| 254 | +2. ✅ 魔法数字 85%+ 是单位转换或配置默认值 | ||
| 255 | +3. ✅ testID 已通过工厂模式集中化 | ||
| 256 | +4. ✅ ESLint 实跑验证 0 个未使用导入 | ||
| 257 | + | ||
| 258 | +**结论**:❌ 全部误报。工具不理解业务语义。 | ||
| 259 | + | ||
| 260 | +## 决策框架:何时修复 vs 何时忽略 | ||
| 261 | + | ||
| 262 | +### 立即修复(P0) | ||
| 263 | + | ||
| 264 | +满足以下全部条件: | ||
| 265 | +- ✅ 行号与描述匹配 | ||
| 266 | +- ✅ 文件存在 | ||
| 267 | +- ✅ 业务上下文验证为真实问题 | ||
| 268 | +- ✅ 实跑工具能复现(tsc / ESLint / test 失败) | ||
| 269 | +- ✅ 修改成本低(< 20 行) | ||
| 270 | + | ||
| 271 | +**示例**: | ||
| 272 | +- TypeScript 编译错误 | ||
| 273 | +- ESLint 报告的语法问题 | ||
| 274 | +- 单元测试失败的 bug | ||
| 275 | + | ||
| 276 | +### 评估后修复(P1) | ||
| 277 | + | ||
| 278 | +满足以下条件: | ||
| 279 | +- ✅ 行号与描述匹配 | ||
| 280 | +- ✅ 文件存在 | ||
| 281 | +- ✅ 业务上下文验证为真实问题 | ||
| 282 | +- ⚠️ 修改成本中等(20-100 行) | ||
| 283 | +- ✅ 有明确的运行时收益 | ||
| 284 | + | ||
| 285 | +**示例**: | ||
| 286 | +- 性能热点优化(有 profiling 数据支持) | ||
| 287 | +- 类型安全改进(消除 `any` 且成本可控) | ||
| 288 | +- API 文档补全(缺失的关键参数说明) | ||
| 289 | + | ||
| 290 | +### 忽略(P2 / Won't Fix) | ||
| 291 | + | ||
| 292 | +满足以下任一条件: | ||
| 293 | +- ❌ 行号与描述不符 | ||
| 294 | +- ❌ 文件不存在 | ||
| 295 | +- ❌ 业务上下文验证为设计而非缺陷 | ||
| 296 | +- ❌ 实跑工具无法复现 | ||
| 297 | +- ❌ 修改成本高于收益 | ||
| 298 | + | ||
| 299 | +**示例**: | ||
| 300 | +- "优化"建议导致代码膨胀 | ||
| 301 | +- 违反项目既有风格 | ||
| 302 | +- 无运行时行为改进 | ||
| 303 | +- 工具配置问题导致的误报 | ||
| 304 | + | ||
| 305 | +## 交叉验证技术(Cross-Validation Techniques) | ||
| 306 | + | ||
| 307 | +### 1. 多工具一致性问题 | ||
| 308 | + | ||
| 309 | +**现象**:Tool A 报告问题,Tool B 未报告。 | ||
| 310 | + | ||
| 311 | +**验证**: | ||
| 312 | +```bash | ||
| 313 | +# 运行多个工具 | ||
| 314 | +pnpm typecheck # TypeScript | ||
| 315 | +pnpm lint # ESLint | ||
| 316 | +pnpm test # Jest | ||
| 317 | +``` | ||
| 318 | + | ||
| 319 | +**决策规则**: | ||
| 320 | +- 仅 1 个工具报告 → 可能是误报,需人工验证 | ||
| 321 | +- 多个工具一致报告 → 高概率真实问题 | ||
| 322 | + | ||
| 323 | +### 2. 代码历史验证 | ||
| 324 | + | ||
| 325 | +**现象**:报告的问题在旧版本中不存在。 | ||
| 326 | + | ||
| 327 | +**验证**: | ||
| 328 | +```bash | ||
| 329 | +# 查看文件历史 | ||
| 330 | +git log --oneline -n 10 <file> | ||
| 331 | + | ||
| 332 | +# 查看具体修改 | ||
| 333 | +git show <commit> -- <file> | grep -A 5 -B 5 "<symbol>" | ||
| 334 | +``` | ||
| 335 | + | ||
| 336 | +**决策规则**: | ||
| 337 | +- 近期重构引入 → 可能是重构副作用,需评估 | ||
| 338 | +- 长期存在 → 可能是有意设计,需理解上下文 | ||
| 339 | + | ||
| 340 | +### 3. 运行时行为验证 | ||
| 341 | + | ||
| 342 | +**现象**:静态分析报告的问题在运行时未出现。 | ||
| 343 | + | ||
| 344 | +**验证**: | ||
| 345 | +```bash | ||
| 346 | +# 添加日志观察 | ||
| 347 | +# 在怀疑位置添加 console.log 或 logger.debug | ||
| 348 | +# 运行实际测试用例 | ||
| 349 | +pnpm test -- --verbose | ||
| 350 | +``` | ||
| 351 | + | ||
| 352 | +**决策规则**: | ||
| 353 | +- 运行时未触发 → 可能是死代码或防御性设计 | ||
| 354 | +- 运行时触发但无影响 → 可能是无害的边界情况 | ||
| 355 | + | ||
| 356 | +### 4. 外部 PR / 补丁评估(PR#7 实战提炼) | ||
| 357 | + | ||
| 358 | +**现象**:收到外部贡献者的 PR(或 codereview 建议),描述看起来都合理,需要判断真伪与是否合入。 | ||
| 359 | + | ||
| 360 | +**验证流程**: | ||
| 361 | +1. **取码不取话**:`git fetch <remote> +refs/merge-requests/<id>/head:pr_N`,逐 commit `git show` 读语义,PR 描述仅作索引 | ||
| 362 | +2. **声明核验**:PR 每条声明对照本地代码找"原始 bug 位置"——找到 bug 才算声明成立 | ||
| 363 | +3. **重叠检测**:`grep` 本地是否已有等价实现(PR#7 的 knownIssue 批次隔离与本地逐行等价 → 跳过) | ||
| 364 | +4. **实测注释加权**:带设备验证记录的补丁(如 KEYCODE 映射注明 TAB=2049 实测焦点遍历)可信度显著更高 | ||
| 365 | +5. **冲突面评估**:`git merge-tree` 预判——PR 基于旧结构 + 本地已大重构(9 文件 changed in both)→ 放弃直接 merge,走手工移植 | ||
| 366 | + | ||
| 367 | +**决策规则**: | ||
| 368 | +- 声明核验全成立 + 本地无替代 → 吸收(手工移植到重构后结构) | ||
| 369 | +- 声明核验时发现本地 bug **比 PR 描述的更严重**(如 forceRefresh 在本地是死参数而非仅 TTL 短路)→ 顺手修本地版,吸收照做 | ||
| 370 | +- 与本地实现重复 → 跳过该条,其余照常评估 | ||
| 371 | + | ||
| 372 | +**常见反转**:PR 声称修复的问题,本地因重构变成了**不同形态**(原单类 → 双子类,bug 也复制成两份且更严重)——核验必须看本地当前代码,不能看 merge-base 处的历史代码。 | ||
| 373 | + | ||
| 374 | +## 项目特定上下文 | ||
| 375 | + | ||
| 376 | +### CPTF 并发模型 | ||
| 377 | + | ||
| 378 | +详见 [`docs/architecture/CONCURRENCY-SAFETY.md`](../../../packages/framework/docs/architecture/CONCURRENCY-SAFETY.md): | ||
| 379 | +- Node.js 单线程事件循环 | ||
| 380 | +- 同步 Map 操作原子性 | ||
| 381 | +- 设备级 key 隔离(`${platform}:${deviceId}`) | ||
| 382 | +- `maxConcurrency` 默认 1(串行执行) | ||
| 383 | +- 并发模式下主动降级(禁用 `setNextTest`) | ||
| 384 | + | ||
| 385 | +### CPTF 编码风格 | ||
| 386 | + | ||
| 387 | +详见 [`AGENTS.md`](../../../AGENTS.md): | ||
| 388 | +- 简洁输出原则 | ||
| 389 | +- `catch {}` 用于 fire-and-forget 模式合理 | ||
| 390 | +- 禁止不必要的注释 | ||
| 391 | +- 配置项应显式列出默认值 | ||
| 392 | + | ||
| 393 | +### CPTF 架构分层 | ||
| 394 | + | ||
| 395 | +详见 [`docs/architecture/ARCHITECTURE.md`](../../../packages/framework/docs/architecture/ARCHITECTURE.md): | ||
| 396 | +- 5 层架构(orchestration → capability → system → infrastructure → base) | ||
| 397 | +- 单向依赖规则 | ||
| 398 | +- 类型定义集中在 `base/types/` | ||
| 399 | + | ||
| 400 | +## 快速检查清单 | ||
| 401 | + | ||
| 402 | +收到优化建议后,按此清单快速评估: | ||
| 403 | + | ||
| 404 | +- [ ] **行号验证**:`sed -n '<start>,<end>p' <file>` 内容与描述匹配 | ||
| 405 | +- [ ] **文件存在**:`ls -la <file>` 确认文件路径正确 | ||
| 406 | +- [ ] **符号搜索**:`grep -n '<symbol>' <file>` 找到真实位置 | ||
| 407 | +- [ ] **上下文理解**:阅读相关代码,理解设计意图 | ||
| 408 | +- [ ] **Concurrency**:确认运行环境 + 隔离机制 + 并发控制 | ||
| 409 | +- [ ] **实跑验证**:`pnpm typecheck && pnpm lint && pnpm test` | ||
| 410 | +- [ ] **成本评估**:修改行数 + 风格一致性 + 运行时收益 | ||
| 411 | +- [ ] **交叉验证**:多个工具一致报告 | ||
| 412 | + | ||
| 413 | +## 总结 | ||
| 414 | + | ||
| 415 | +**核心原则**: | ||
| 416 | +1. **行号漂移是常态**:静态工具的行号经常错位,必须实跑验证 | ||
| 417 | +2. **上下文决定一切**:Node.js 单线程、设备隔离、设计模式等上下文决定问题真实性 | ||
| 418 | +3. **成本收益权衡**:不是所有"问题"都值得修复,过度优化会损害代码质量 | ||
| 419 | +4. **交叉验证**:多个工具一致报告的可信度远高于单一工具 | ||
| 420 | + | ||
| 421 | +**避免的错误**: | ||
| 422 | +- ❌ 盲目相信静态分析报告 | ||
| 423 | +- ❌ 不验证行号就修复 | ||
| 424 | +- ❌ 追求"零警告"而引入噪音代码 | ||
| 425 | +- ❌ 忽略项目既有风格和上下文 | ||
| 426 | + | ||
| 427 | +**正确的做法**: | ||
| 428 | +- ✅ 先验证,后修复 | ||
| 429 | +- ✅ 理解业务语义和运行环境 | ||
| 430 | +- ✅ 评估成本收益比 | ||
| 431 | +- ✅ 尊重项目既有风格 | ||
| @@ -1,11 +1,11 @@ | |||
| 1 | --- | 1 | --- |
| 2 | name: cptf-refactor-rules | 2 | name: cptf-refactor-rules |
| 3 | -description: Use when refactoring CPTF (packages/framework) or applying lessons learned from v0.2.1 refactor. Contains concrete rules for: splitting God Class into Mixin, eliminating `as any` in business code, async-IO migration, shell command normalization, AbortController race cancellation, and template / file-I-O separation. Triggers: "refactor", "split mixin", "god class", "拆分", "重构", "as any", "async io", "shell command", "raceWithCancel", "tryWithTimeout", "index template", "report builder", "@internal", "verify check". Do NOT use for new feature implementation (use cptf-framework-dev) or test writing (use cptf-test-api). | 3 | +description: Use when refactoring CPTF (packages/framework) or applying lessons learned from v0.2.1 refactor. Contains concrete rules for: splitting God Class into Mixin, eliminating `as any` in business code, async-IO migration, shell command normalization, AbortController race cancellation, template / file-I-O separation, dead-code保留/删除判定(git 溯源), 复制分叉 bug 检测, 多重继承 protected 可见性. Triggers: "refactor", "split mixin", "god class", "拆分", "重构", "as any", "async io", "shell command", "raceWithCancel", "tryWithTimeout", "index template", "report builder", "@internal", "verify check", "dead code", "死代码", "保留", "删除判定", "copy fork", "复制分叉". Do NOT use for new feature implementation (use cptf-framework-dev) or test writing (use cptf-test-api). |
| 4 | --- | 4 | --- |
| 5 | 5 | ||
| 6 | # CPTF Refactor Rules (v0.2.1+) | 6 | # CPTF Refactor Rules (v0.2.1+) |
| 7 | 7 | ||
| 8 | -> 来源:v0.2.1 重构期间(OPTIMIZATION-ANALYSIS.md 24/26 任务)形成的具体约束、反模式与标准做法。 | 8 | +> 来源:v0.2.1 重构期间(v0.2.1 重构(24/26 优化任务,详见 CHANGELOG))形成的具体约束、反模式与标准做法。 |
| 9 | > 适用:未来重构 packages/framework 时遵循,避免重复踩坑。 | 9 | > 适用:未来重构 packages/framework 时遵循,避免重复踩坑。 |
| 10 | 10 | ||
| 11 | --- | 11 | --- |
| @@ -266,28 +266,10 @@ async function writeScenePartitionedIndex(cases, outDir, builder) { | |||
| 266 | ### 反模式 | 266 | ### 反模式 |
| 267 | ComparisonRunner 1116 行,UI 对比 + 报告生成 + 邮件发送全部混在一起。 | 267 | ComparisonRunner 1116 行,UI 对比 + 报告生成 + 邮件发送全部混在一起。 |
| 268 | 268 | ||
| 269 | -### 修复(已落地) | 269 | +### 修复状态 |
| 270 | -拆为 2 个独立组件: | 270 | +**未单独抽取**。`UIComparisonService` / `ComparisonReportBuilder` 作为独立文件**未创建**(实际代码在 `comparison-runner.ts` 内联实现)。如下游需要单独测试/复用,可后续抽取。 |
| 271 | 271 | ||
| 272 | -```ts | 272 | +### TestRunner 拆分(已落地) |
| 273 | -// orchestration/ui-comparison-service.ts | ||
| 274 | -export class UIComparisonService { | ||
| 275 | - async startEarlyUIComparisons(): Promise<void> { /* 队列调度 */ } | ||
| 276 | - async waitEarlyUIComparisons(): Promise<void> { /* 等待 */ } | ||
| 277 | - compareAcrossPlatforms(): ComparisonResult[] { /* 跨平台一致性 */ } | ||
| 278 | -} | ||
| 279 | - | ||
| 280 | -// orchestration/comparison-report-builder.ts | ||
| 281 | -export class ComparisonReportBuilder { | ||
| 282 | - async build(results: ComparisonResult[]): Promise<ComparisonReport> { | ||
| 283 | - // 包含 QualityReport 计算(多维度评分 + 覆盖统计) | ||
| 284 | - return { meta, summary, results, recommendations, qualityReport }; | ||
| 285 | - } | ||
| 286 | - private buildQualityReport(): QualityReport { /* ... */ } | ||
| 287 | -} | ||
| 288 | -``` | ||
| 289 | - | ||
| 290 | -### TestRunner 拆分(保守) | ||
| 291 | - 抽出 `HealthKeeper`(keepalive + 健康检查) | 273 | - 抽出 `HealthKeeper`(keepalive + 健康检查) |
| 292 | - 抽出 `ReportOrchestrator`(HTML/MD/JSON 报告编排 + 邮件) | 274 | - 抽出 `ReportOrchestrator`(HTML/MD/JSON 报告编排 + 邮件) |
| 293 | - 主类保留:`run()` / `runTestCase()` / `executeTestCase()` 主体(跨方法状态依赖紧密,强行拆会引入更多复杂度) | 275 | - 主类保留:`run()` / `runTestCase()` / `executeTestCase()` 主体(跨方法状态依赖紧密,强行拆会引入更多复杂度) |
| @@ -392,7 +374,114 @@ Coverage: 2 ✓ | |||
| 392 | 374 | ||
| 393 | --- | 375 | --- |
| 394 | 376 | ||
| 395 | -## 12. 重构决策模板 | 377 | +## 12. Strategy 模式替换 if 分支(v0.3.0 已完成) |
| 378 | + | ||
| 379 | +### 历史反模式(v0.3.0 之前,已修复) | ||
| 380 | +```ts | ||
| 381 | +// system/harmony/mixins/input-controller.ts(旧版,已删除) | ||
| 382 | +async click(x: number, y: number): Promise<boolean> { | ||
| 383 | + if (this.native) { try { await this.native.click(x, y); return true; } catch (e) { ... } } // 路径1 | ||
| 384 | + if (this.useDriver) { try { await this.host.runner.driver.click(x, y); ... } } // 路径2 | ||
| 385 | + const ok = await this.touchTap(x, y); // 路径3 | ||
| 386 | + return ok; | ||
| 387 | +} | ||
| 388 | +``` | ||
| 389 | + | ||
| 390 | +**历史问题**: | ||
| 391 | +- 业务代码 17 处 `if (this.native)` 分支(input-controller.ts + layout-provider.ts) | ||
| 392 | +- 同一函数体 3 路径 + try/catch 降级 | ||
| 393 | +- 报告 `source` 字段混用 native/hmd/uitest,难归因 | ||
| 394 | + | ||
| 395 | +### 当前实现(v0.3.0) | ||
| 396 | +**抽象 + 构造时定型**: | ||
| 397 | +```ts | ||
| 398 | +// system/harmony/mixins/input-controller.ts(当前) | ||
| 399 | +export abstract class HarmonyInputController extends BaseSystemMixin<HarmonySystem> { | ||
| 400 | + abstract click(x: number, y: number): Promise<boolean>; | ||
| 401 | + abstract clickByTestId(testID: string): Promise<boolean>; | ||
| 402 | + abstract swipe(...): Promise<boolean>; | ||
| 403 | + // ... 15 abstract methods | ||
| 404 | +} | ||
| 405 | + | ||
| 406 | +export class NativeInputController extends HarmonyInputController { | ||
| 407 | + private get driver(): HarmonyNativeDriver { | ||
| 408 | + return this.host.nativeDriver as HarmonyNativeDriver; // 从 host 读,不通过构造器注入 | ||
| 409 | + } | ||
| 410 | + async click(x, y) { return await this.driver.click(x, y); } | ||
| 411 | + async clickByTestId(t) { return await this.driver.clickComponent(t); } | ||
| 412 | + async swipe(x1, y1, x2, y2, d) { return await this.driver.swipe(x1, y1, x2, y2, d); } | ||
| 413 | +} | ||
| 414 | + | ||
| 415 | +export class ExternalInputController extends HarmonyInputController { | ||
| 416 | + async click(x, y) { | ||
| 417 | + if (this.useDriver) { /* HMD RPC */ } | ||
| 418 | + return await this.touchTap(x, y); | ||
| 419 | + } | ||
| 420 | + async clickByTestId(t) { | ||
| 421 | + const el = await this.host.layout.getElementByTestId(t, 3000); | ||
| 422 | + return el ? await this.click(el.bounds.center.x, el.bounds.center.y) : false; | ||
| 423 | + } | ||
| 424 | +} | ||
| 425 | +``` | ||
| 426 | + | ||
| 427 | +**主类构造时定型**(`HarmonySystem`,当前): | ||
| 428 | +```ts | ||
| 429 | +class HarmonySystem extends BaseSystem { | ||
| 430 | + input: HarmonyInputController; | ||
| 431 | + layout: HarmonyLayoutProvider; | ||
| 432 | + perf: HarmonyPerformanceProvider; | ||
| 433 | + // ... 6 个策略类 | ||
| 434 | + | ||
| 435 | + constructor(config: SystemConfig) { | ||
| 436 | + super(config); | ||
| 437 | + this.layout = config.platformOptions?.['driver'] === 'native' | ||
| 438 | + ? new NativeLayoutProvider(this) | ||
| 439 | + : new ExternalLayoutProvider(this); | ||
| 440 | + this.input = config.platformOptions?.['driver'] === 'native' | ||
| 441 | + ? new NativeInputController(this) | ||
| 442 | + : new ExternalInputController(this); | ||
| 443 | + this.perf = config.platformOptions?.['driver'] === 'native' | ||
| 444 | + ? new NativePerformanceProvider(this) | ||
| 445 | + : new ExternalPerformanceProvider(this); | ||
| 446 | + // ... 6 个 mixin 全部按 driverMode 选 | ||
| 447 | + } | ||
| 448 | +} | ||
| 449 | +``` | ||
| 450 | + | ||
| 451 | +**Android 类名前缀**:`AndroidNativeInputController` / `AndroidExternalInputController`(注意 `Android` 前缀) | ||
| 452 | + | ||
| 453 | +### 验证 | ||
| 454 | +- ✅ `grep -n "if (this.isNativeMode)\|if (this.native)" packages/framework/src/system/harmony/mixins/` 返回 0 | ||
| 455 | +- ✅ `grep -n "if (this.isNativeMode)\|if (this.native)" packages/framework/src/system/android/mixins/` 返回 0 | ||
| 456 | +- ✅ `pnpm typecheck && pnpm test && pnpm verify` 全过 | ||
| 457 | + | ||
| 458 | +### 4 级降级(v0.3.0 状态) | ||
| 459 | + | ||
| 460 | +| Level | 位置 | 切模式 | 范围 | 状态 | | ||
| 461 | +|---|---|---|---|---| | ||
| 462 | +| L1 Pre-flight | `DriverModeArbiter`(`harmony/driver-mode-arbiter.ts`)| ✅ 整 run 1 次 | run | ✅ 类已实现,⚠️ 未接入 TestRunner | | ||
| 463 | +| L2 Reconnect | `HarmonyNativeDriver.tryReconnect()` 指数退避 1s/2s/4s × 3 30s 预算 | ❌ 仍 native | 单 case | ✅ Harmony 已实现,❌ Android 待补 | | ||
| 464 | +| L3 Action Retry | `HarmonyNativeDriver.sendCommand()` 内部 1 次重试 | ❌ 仍 native | 单 action | ✅ Harmony 已实现,❌ Android 待补 | | ||
| 465 | +| L4 Crash Recovery | `ISystem.tryNativeCrashRecovery` / `HarmonyNativeDriver.recoverFromCrash(bundleId)` | ❌ 仍 native | 单 case | ✅ 双平台已实现并接入 TestRunner | | ||
| 466 | + | ||
| 467 | +**禁止操作期静默切模式**(破坏隔离、报告难归因)。 | ||
| 468 | + | ||
| 469 | +### 当前保留的兼容方法 | ||
| 470 | + | ||
| 471 | +- `HarmonyNativeDriver._permanentlyFailed` 标记(`HarmonyNativeDriver`)— 仍在用,标记 native driver 进入永久失败状态 | ||
| 472 | +- `AndroidNativeDriver._permanentlyFailed` 标记(`AndroidNativeDriver`)— 同上 | ||
| 473 | +- `UitestRunner._permanentlyFailed` 标记(`UitestRunner`)— UITest 重连失败标记 | ||
| 474 | +- `IosWdaClient._permanentlyFailed` 标记(`IosWdaClient`)— WDA 重连失败标记 | ||
| 475 | +- `AndroidU2Client._permanentlyFailed` 标记(`AndroidU2Client`)— U2 重连失败标记 | ||
| 476 | + | ||
| 477 | +**这些 `_permanentlyFailed` 是 connection-level 永久失败标记,与 L2 重连预算(L2 budget)是不同维度**: | ||
| 478 | +- `_permanentlyFailed=true` 表示连接已无法恢复(多次重连失败) | ||
| 479 | +- L2 budget 是每次断开后的重试次数(3 次指数退避 30s) | ||
| 480 | +- 两者并存:先尝试 L2 budget,失败后设 `_permanentlyFailed=true`,后续直接走 shell 降级 | ||
| 481 | + | ||
| 482 | +--- | ||
| 483 | + | ||
| 484 | +## 13. 重构决策模板 | ||
| 396 | 485 | ||
| 397 | 遇到 1000+ 行文件时按此顺序判断: | 486 | 遇到 1000+ 行文件时按此顺序判断: |
| 398 | 487 | ||
| @@ -407,9 +496,228 @@ Coverage: 2 ✓ | |||
| 407 | 496 | ||
| 408 | --- | 497 | --- |
| 409 | 498 | ||
| 499 | + | ||
| 500 | +## 14. God class 机械拆分流程(0.3.1 实战沉淀) | ||
| 501 | + | ||
| 502 | +拆分 1000+ 行类时的安全流程(已应用于 comparison-report-generator / expect-executor / test-scene-navigator / TestRunner 的拆分)。 | ||
| 503 | + | ||
| 504 | +### 拆分前(决定拆什么) | ||
| 505 | + | ||
| 506 | +1. **死代码审计先行**:`grep -rn "\.method(" src --include="*.ts" | grep -v 定义文件` 零调用的方法不拆、直接删(曾把 `generateReportStreaming` 拆到新文件后才发现 HEAD 起就零调用,连带 300 行 StreamingWriter 全链死) | ||
| 507 | +2. **调用面编目**:列出 facade 必须保留的公共 API(外部调用点 grep 确定),其余全部下沉 | ||
| 508 | +3. **按职责定形态**:3 种输出格式 → 3 个类;纯函数域(颜色/元素属性解析)→ 模块级函数文件;组合体(导航)→ Core 接口 + Part 基类 + 2-3 个 Part 类 + 薄 facade 委托 | ||
| 509 | + | ||
| 510 | +### 提取中(防转录错误) | ||
| 511 | + | ||
| 512 | +4. **从 git HEAD 取原始源**:工作区可能处于中间态,`git show HEAD:path > /tmp/orig.ts` 后按行号提取,禁止凭记忆重写 | ||
| 513 | +5. **边界双断言**:提取段首行必须是方法签名、尾行必须是 ` }`(方法闭合);python 提取脚本对两断言 assert,失败即停 | ||
| 514 | +6. **签名适配最小化**:`this.xxx()` 调用点用机械字符串替换(this.ui→core.ui),私有方法签名改 public/export;参数化时闭包变量显式进签名(startTime 等) | ||
| 515 | +7. **替换必须验数**:python `str.replace` 对含 `\n`/反引号的模板字符串会静默失配——每次替换前 `assert old in src`、替换后核对 count(曾因 `\n` 转义不匹配导致转义修复静默失败,下轮审计才发现) | ||
| 516 | + | ||
| 517 | +### 拆分后(清扫清单) | ||
| 518 | + | ||
| 519 | +8. **未用 import 扫描**:机械拆分必然残留(renderSection/DeviceInfo/Writable 类);简易法:import 符号在文件内出现次数 ≤1 即删 | ||
| 520 | +9. **过度导出收敛**:仅文件内使用的函数去 export(拆分时倾向全 export,收尾要回收) | ||
| 521 | +10. **重复逻辑收编**:同一逻辑在旧文件/新文件各一份时(设备 map 构建曾 3 份),保留一份、其余改调用 | ||
| 522 | +11. **验证门**:每阶段 typecheck + jest + verify;tsc 偶发单次语法错误(WSL 文件写入竞态)先复跑再调试 | ||
| 523 | + | ||
| 524 | +### 测试配套陷阱(u2-race 实战) | ||
| 525 | + | ||
| 526 | +12. **mock 高层方法丢副作用**:真实 `connect()` 成功时自置 `_connected=true`;`mockImplementation` 替换后副作用消失 → 断言必挂。改 mock 底层原语(launchU2Server/waitForServerReady/callRpc) | ||
| 527 | +13. **构造期 fire-and-forget 击穿实例 spy**:构造函数里 `this.initU2()`(无 await)先于测试的 `jest.spyOn` 执行 → 实例级 mock 永远晚一步。必须 mock 原型方法(`AndroidU2Client.prototype.xxx`)或底层原语 | ||
| 528 | +14. **pending promise 时序**:hold 型 mock(外部 resolve)需要 `waitRpcReady()` 轮询辅助函数等待 promise 真正 pending 后再 resolve,否则 resolve 调用在 nobody listen 时为 no-op → 测试超时 | ||
| 529 | + | ||
| 530 | +--- | ||
| 531 | + | ||
| 532 | +## 15. 死代码判定:保留 vs 删除(git 溯源 + 保留/删除决策树) | ||
| 533 | + | ||
| 534 | +> 来源:v0.3.1 PRINCIPLES-AUDIT 复核。教训:原审计报告把 9 项零调用代码全标"删",实际其中 9 项(Synchronizer / plugin-rn / AccessibilityAnalyzer / RunnerStrategyRegistry / KNOWN_REPORT_TYPES / NavigationStrategyFactory / ReportGeneratorRegistry 方法 / MetricCollectorFactory 方法 / 预留类型 + ConfigManager 预留字段)都是 **ec1f5af 插件化批次引入的预留能力**,被错杀。 | ||
| 535 | + | ||
| 536 | +### 核心方法论:删除前必须 git 溯源 | ||
| 537 | + | ||
| 538 | +不能因为"零调用"就删除——必须先回答**"为什么没用起来"**,否则会误杀预留能力。 | ||
| 539 | + | ||
| 540 | +```bash | ||
| 541 | +# 1. 找到引入提交 | ||
| 542 | +git log --diff-filter=A --oneline -- <file> | ||
| 543 | + | ||
| 544 | +# 2. 找到失去消费方的提交 | ||
| 545 | +git log --oneline -S "<SymbolName>" --all -- <path> | ||
| 546 | + | ||
| 547 | +# 3. 看引入批次的 commit message(理解引入意图) | ||
| 548 | +git show <commit-introducer> --stat | rg "<symbol>" | ||
| 549 | +git log -1 --format="%B" <commit-introducer> | ||
| 550 | +``` | ||
| 551 | + | ||
| 552 | +### 保留/删除决策树 | ||
| 553 | + | ||
| 554 | +按顺序判断(命中任一即停): | ||
| 555 | + | ||
| 556 | +| 信号 | 来源 | 处置 | | ||
| 557 | +|---|---|---| | ||
| 558 | +| 引入批次是 `feat: 重构` / `feat(God class 拆分)` / `feat: 全量修复+报告审计` 等重构提交 | git log | **删除**(重构副产物) | | ||
| 559 | +| 类型/接口被另一个活跃类型完全替代(TestResult 取代 TestResultInternal) | rg 全仓 | **删除** | | ||
| 560 | +| 注释自述"预留配置" / "尚未在运行时引用" / "Reserved" | 文件内 | **保留**(标注预留) | | ||
| 561 | +| 类型/class 注释自述 `user-extensible` / `third-party` / `plugin` | 文件内 | **保留**(extension contract) | | ||
| 562 | +| docstring 示例引用了某个 import,但该 import 从未在 `index.ts`/`internal.ts` 导出 | rg 检查 | **保留 + 修复接线遗漏** | | ||
| 563 | +| 配套 test 文件覆盖完整(assertion 数 > 50)但生产零消费 | 引入批次是 `feat: <能力>` | **保留**(等待接线,预留能力) | | ||
| 564 | +| 被 `internal.ts` 导出但 `index.ts` 不导出 | 查两文件 | **保留**(深度入口,不需要导出到 `index.ts`) | | ||
| 565 | + | ||
| 566 | +### 典型引入批次与判定 | ||
| 567 | + | ||
| 568 | +| 引入提交 | 内容 | 判定 | 理由 | | ||
| 569 | +|---|---|---|---| | ||
| 570 | +| `8248907` Phase-0 DeviceBridge | Synchronizer + schema probe types | **保留** | 双模式架构预留能力 | | ||
| 571 | +| `ec1f5af` benchmark 插件化 | plugin-rn / AccessibilityAnalyzer / RunnerStrategyRegistry / KNOWN_REPORT_TYPES / 注册表门面方法 / ConfigManager 预留字段 / 预留类型 | **全部保留** | 同一批插件化扩展点,用户已表态保留 plugin-rn → 同批全保留 | | ||
| 572 | +| `616bd2b` / `716c916` 0.3.1 重构 | buildSparklineSvg(提取副产物)/ 被取代的内部形状(TestResultInternal 等)/ 内联后遗留的工具函数 | **删除** | 重构提取只接线了 buildSvgChart 等活跃部分;buildSparklineSvg 等为提取副产物 | | ||
| 573 | + | ||
| 574 | +### 操作清单(删除前必做) | ||
| 575 | + | ||
| 576 | +1. **三方搜索全消费方**(不仅框架 src): | ||
| 577 | + ```bash | ||
| 578 | + rg -rn "<SymbolName>" app/ cptf/packages/ --glob '*.ts' --glob '*.tsx' | rg -v node_modules | rg -v <自身文件> | ||
| 579 | + ``` | ||
| 580 | +2. **检查公共出口**:`index.ts` 和 `internal.ts` 是否导出(决定删除是否破坏公共 API) | ||
| 581 | +3. **检查配套测试**:test 文件是否引用(决定是否连带删除) | ||
| 582 | +4. **检查 docstring 示例**:example code 是否引用(决定是否示例编译失败——这是接线遗漏信号) | ||
| 583 | +5. **删除时同步清理**:unused import、unused type export、同名的死 alias | ||
| 584 | + | ||
| 585 | +### 反例:什么场景下"看起来死"实际是预留 | ||
| 586 | + | ||
| 587 | +| 看起来 | 实际 | | ||
| 588 | +|---|---| | ||
| 589 | +| `registerRNProbes` 零调用 | 插件化扩展点;plugin-rn 包预留 | | ||
| 590 | +| `AccessibilityAnalyzer` 仅测试存活(439 行测试) | 完整 a11y 规则引擎 + 测试,对应 app 无障碍用例 `unsupported` 恢复计划 | | ||
| 591 | +| `RunnerStrategyRegistry` 零调用 | 文档示例引用 `@cptf/framework/internal` 导出但**从未导出**——这是**接线遗漏**,需补 internal.ts 导出而非删除 | | ||
| 592 | +| `ConfigManager.device/cacheTimeout/report/retryPolicy` | 注释自述"预留配置",对应重连/缓存/报告/重试策略的未来接线 | | ||
| 593 | +| `KNOWN_REPORT_TYPES` 零引用 | 报告插件契约常量,文档化的 extension API | | ||
| 594 | + | ||
| 595 | +--- | ||
| 596 | + | ||
| 597 | +## 16. 复制分叉是系统性 bug 源(抽基类的"信号识别") | ||
| 598 | + | ||
| 599 | +> 来源:v0.3.1 PRINCIPLES-AUDIT 清理期间连续发现 4 个真 bug,均为 harmony/android 逐行复制分叉的产物: | ||
| 600 | +> - BUG-1 `swipe(x, y, endY, endY)` — 应为 `(x, y, endX, endY)` | ||
| 601 | +> - `click` 缺 `await`(所有兄弟方法及 android 版均有 await) | ||
| 602 | +> - `RandomAction` 模板 40 行复制 | ||
| 603 | +> - `scrollTo` 几何 16 行复制 | ||
| 604 | + | ||
| 605 | +### 抽基类的判断信号 | ||
| 606 | + | ||
| 607 | +任何**两条**满足即应抽基类: | ||
| 608 | + | ||
| 609 | +1. `rg -n "<methodName>" src/system/<a> src/system/<b>` 出现完全相同的实现块(≥ 10 行) | ||
| 610 | +2. 该方法有 `async` + `try/catch` 或 `setTimeout`/资源管理逻辑 | ||
| 611 | +3. (加分项)兄弟方法已经存在但本方法缺关键调用(如 `await`/`clearTimeout`) | ||
| 612 | + | ||
| 613 | +### 实操:发现复制分叉后 | ||
| 614 | + | ||
| 615 | +```bash | ||
| 616 | +# 1. 找重复块 | ||
| 617 | +diff <(sed -n '/async randomAction/,/^ }/p' src/system/harmony/mixins/input-controller.ts) \ | ||
| 618 | + <(sed -n '/async randomAction/,/^ }/p' src/system/android/mixins/input-controller.ts) | ||
| 619 | + | ||
| 620 | +# 2. 查 iOS 是否有第三变体(可能差异更大) | ||
| 621 | +rg -n "async randomAction" src/system/ios/mixins/ | ||
| 622 | + | ||
| 623 | +# 3. 抽到 base-system-mixin.ts 的 default* 模板 + 子类提供 ops hook | ||
| 624 | +``` | ||
| 625 | + | ||
| 626 | +### 抽基类后必须验证 | ||
| 627 | + | ||
| 628 | +1. **import 链**:删除子类原方法后是否还有 import 引用 | ||
| 629 | +2. **iOS 第三变体**:如果 iOS 不同,保留 override(避免破坏 iOS 语义) | ||
| 630 | +3. **method call site**:子类业务方法调 `this.xxx()` 是否仍合法(尤其 `protected` 可见性,见下节) | ||
| 631 | +4. **等价性逐项对比**:每个删除的代码块与重构版做行为对比,记录任何不等价点(错误消息文案、调试日志等) | ||
| 632 | + | ||
| 633 | +### 反例:不要被"零调用 + 无文档"误导 | ||
| 634 | + | ||
| 635 | +不是所有重复代码都是 bug——只有当**两个平台同步漂移**时才需要抽基类。如果只有一处实现(即便 100 行重复),先单独看那处是否有问题;不要为抽而抽。 | ||
| 636 | + | ||
| 637 | +--- | ||
| 638 | + | ||
| 639 | +## 17. TypeScript 多重继承 + protected 可见性陷阱 | ||
| 640 | + | ||
| 641 | +> 来源:v0.3.1 WsRpcClient 基类抽取时踩坑。 | ||
| 642 | + | ||
| 643 | +### 陷阱描述 | ||
| 644 | + | ||
| 645 | +抽 `WsRpcClient extends EventEmitter` 作为基类,子类 `HarmonyNativeDriver` 需要继承基类的方法: | ||
| 646 | + | ||
| 647 | +```ts | ||
| 648 | +// ❌ 错误:子类只 extends EventEmitter,基类 protected 方法不可见 | ||
| 649 | +export class HarmonyNativeDriver extends EventEmitter { | ||
| 650 | + async click(...) { | ||
| 651 | + await this.sendCommand(...); // TS: Property 'sendCommand' does not exist | ||
| 652 | + } | ||
| 653 | +} | ||
| 654 | + | ||
| 655 | +// ✅ 正确:子类必须 extends WsRpcClient(即使已间接 extends EventEmitter) | ||
| 656 | +export class HarmonyNativeDriver extends WsRpcClient { | ||
| 657 | + // ... | ||
| 658 | +} | ||
| 659 | +``` | ||
| 660 | + | ||
| 661 | +### 核心规则 | ||
| 662 | + | ||
| 663 | +**子类必须显式 extends 基类**——TypeScript 不会从 mixin/参数化类型推断出继承关系。即使基类的方法通过 `protected` 标记对**未来子类**可见,**当前类**若没 extends 该基类,protected 方法不存在于自身的 type 中。 | ||
| 664 | + | ||
| 665 | +### 抽基类的 class 签名模板 | ||
| 666 | + | ||
| 667 | +```ts | ||
| 668 | +// 基类:extends 必要父类(EventEmitter / BaseSystem / 等) | ||
| 669 | +export abstract class WsRpcClient extends EventEmitter { | ||
| 670 | + private pendingRequests: Map<string, PendingRequest> = new Map(); | ||
| 671 | + protected requestIdCounter = 0; | ||
| 672 | + | ||
| 673 | + // 抽象方法由子类提供 | ||
| 674 | + protected abstract getWs(): WebSocket | null; | ||
| 675 | + protected abstract shouldRetry(err: Error): boolean; | ||
| 676 | + protected abstract isDestroyed(): boolean; | ||
| 677 | + | ||
| 678 | + // 模板方法:protected 允许子类调用 | ||
| 679 | + protected async sendCommand(action: string, ...): Promise<unknown> { | ||
| 680 | + // 模板实现 | ||
| 681 | + } | ||
| 682 | + | ||
| 683 | + // Hook 方法:protected,子类按需 override | ||
| 684 | + protected async preSend(_action: string): Promise<void> { /* noop */ } | ||
| 685 | + protected wrapResolve(_action: string, resolve: (v: unknown) => void, _timer: NodeJS.Timeout): (v: unknown) => void { | ||
| 686 | + return resolve; | ||
| 687 | + } | ||
| 688 | +} | ||
| 689 | +``` | ||
| 690 | + | ||
| 691 | +### 子类覆盖规则 | ||
| 692 | + | ||
| 693 | +```ts | ||
| 694 | +// ✅ protected override 必须与基类签名一致 | ||
| 695 | +protected override getWs(): WebSocket | null { return this.driverWs; } | ||
| 696 | +protected override shouldRetry(err: Error): boolean { return isRetryableError(err); } | ||
| 697 | + | ||
| 698 | +// ❌ 不要把基类方法改成 private——会报 "is private in type X but not in type Y" | ||
| 699 | +private rejectAllPending(err: Error): void { ... } | ||
| 700 | + | ||
| 701 | +// ❌ 不要重复定义基类已有的接口(命令/PendingRequest 等)—— 抽到 base | ||
| 702 | +interface CommandMessage { ... } // 删,引用基类 WsRpcCommand | ||
| 703 | +``` | ||
| 704 | + | ||
| 705 | +### TS 编译错误的精确诊断 | ||
| 706 | + | ||
| 707 | +抽基类后常见 TS 错误 → 原因映射: | ||
| 708 | + | ||
| 709 | +| 错误信息 | 根因 | 修正 | | ||
| 710 | +|---|---|---| | ||
| 711 | +| `Property 'X' does not exist on type 'Y'` | 子类没 extends 基类 / 基类方法签名未声明 | 检查 `class Y extends X` + 基类方法可见性 | | ||
| 712 | +| `'Y' is private in type 'X' but not in type 'Z'` | 基类是 protected,子类声明了 private 同名 | 删子类的 private 声明,依赖基类继承 | | ||
| 713 | +| `Class 'X' incorrectly extends base class` | 多重继承中子类保留父类的 private 字段 | 把 private 字段提到基类 protected 或删 | | ||
| 714 | +| `Argument of type 'A[]' is not assignable to parameter of type 'B[]'` | A 与 B 缺索引签名兼容 | 接口加 `[key: string]: T` 索引签名,或调用点 cast `as unknown as B[]` | | ||
| 715 | + | ||
| 716 | +--- | ||
| 717 | + | ||
| 410 | ## 相关 Skill | 718 | ## 相关 Skill |
| 411 | 719 | ||
| 412 | -- `cptf-arch` — 3层架构 + 类型位置(v0.2.1 之前定义) | 720 | +- `cptf-arch` — 5 层架构 + 类型位置 |
| 413 | - `cptf-framework-dev` — 改 packages/framework 时的具体规则 | 721 | - `cptf-framework-dev` — 改 packages/framework 时的具体规则 |
| 414 | - `cptf-system-extension` — 新平台/ISystem 子接口(继承自 mixin 模式) | 722 | - `cptf-system-extension` — 新平台/ISystem 子接口(继承自 mixin 模式) |
| 415 | - `cptf-stability` — 稳定性 detector 配置 | 723 | - `cptf-stability` — 稳定性 detector 配置 |
| @@ -1,112 +1,161 @@ | |||
| 1 | --- | 1 | --- |
| 2 | name: cptf-report | 2 | name: cptf-report |
| 3 | -description: Use when customizing HTML / Markdown / JSON reports, integrating trend charts, or understanding ReportService / ReportJobQueue / off-thread report generation. Triggers: "HtmlReportGenerator", "MarkdownTestReportGenerator", "ComparisonReportGenerator", "ReportService", "ReportJobQueue", "TrendChart", "trend points", "report custom", "JUnit XML", "CI integration". Do NOT use for general test writing (cptf-test-api) or CLI invocation (cptf-cli). | 3 | +description: Use when customizing HTML / Markdown / JSON reports, integrating trend charts, or understanding ReportService / ReportJobQueue / off-thread report generation. Triggers: "HtmlReportGenerator", "MarkdownTestReportGenerator", "ComparisonReportGenerator", "ReportService", "ReportJobQueue", "TrendChart", "trend points", "report custom", "JUnit XML", "CI integration", "escapeHtml", "unifiedMetrics", "computeUnifiedMetrics", "report escape", "chart-renderer", "ComparisonReport". Do NOT use for general test writing (cptf-test-api) or CLI invocation (cptf-cli). |
| 4 | --- | 4 | --- |
| 5 | 5 | ||
| 6 | -# Report Customization Skill | 6 | +# CPTF 报告模块规则 (v0.3.1+) |
| 7 | 7 | ||
| 8 | -## Built-in Reports (v0.2.0) | 8 | +> 报告模块位于 `packages/framework/src/orchestration/report/`。本文件是该模块的"硬约束 + 速查",所有改报告代码必须遵守。 |
| 9 | +> 准则来源:CORE-PRINCIPLES(准确性第一)+ METRICS-SPEC(指标单一真相)+ v0.3.1 报告审计修复。 | ||
| 9 | 10 | ||
| 10 | -| Format | Generator | Output | | 11 | +## 1. 模块结构 |
| 11 | -|--------|-----------|--------| | ||
| 12 | -| `html` | `HtmlReportGenerator` | `test-report.html` (full HTML with trend charts) | | ||
| 13 | -| `json` | (built-in) | `test-report.json` (raw data) | | ||
| 14 | -| `markdown` | `MarkdownTestReportGenerator` | `test-report.md` (concise summary) | | ||
| 15 | -| comparison | `ComparisonReportGenerator` | `comparison-report.html/md/json` | | ||
| 16 | 12 | ||
| 17 | -## ReportService (v0.2.0) | 13 | +| 文件 | 职责 | 行数上限 | |
| 14 | +|------|------|---------| | ||
| 15 | +| `report-theme.ts` | 调色板 + 布局常量(单一真相) | ≤ 80 | | ||
| 16 | +| `report-css.ts` | `SHARED_CSS` / `TEST_REPORT_EXTRA_CSS` / `COMPARISON_REPORT_EXTRA_CSS` | ≤ 350 | | ||
| 17 | +| `report-utils.ts` | 转义/格式化纯函数:`escapeHtml` `escapeMdCell` `sanitizeHtmlId` `formatMemory` `formatDuration` | ≤ 80 | | ||
| 18 | +| `html-generator.ts` | 单平台 HTML 报告(编排 + 详情卡) | ≤ 1000 | | ||
| 19 | +| `chart-renderer.ts` | SVG 图表纯函数(buildSvgChart/buildSparklineSvg/buildTrendTableRows/sampleData/niceScale/computeTrendStats/lightenColor) | ≤ 300 | | ||
| 20 | +| `markdown-test-generator.ts` | 单平台 Markdown 报告 | ≤ 700 | | ||
| 21 | +| `comparison-report-generator.ts` | 跨平台对比报告(HTML/MD/JSON) | ≤ 1200(God class 待继续拆) | | ||
| 22 | +| `metrics-computer.ts` | `computeUnifiedMetrics` + `buildQualityReport` + 违规检测(指标单一真相源) | ≤ 750 | | ||
| 23 | +| `report-orchestrator.ts` | TestReport 报告编排(async/sync + 邮件) | ≤ 200 | | ||
| 24 | +| `report-service.ts` | ReportJobQueue 门面 | ≤ 200 | | ||
| 25 | +| `html-components.ts` | 可复用 HTML 片段 + 过滤器 JS(`renderTableFilterBar`/`renderTableFilterScript`) | ≤ 300 | | ||
| 26 | +| `report-generator-registry.ts` | 生成器注册(test-html/json/markdown, comparison-*) | ≤ 300 | | ||
| 18 | 27 | ||
| 19 | -Batches multiple report formats for **concurrent off-thread generation** via `ReportJobQueue`: | 28 | +**入口**:单平台 HTML 经 `report-generator-registry.ts:createTestHtmlGenerator` → `HtmlReportGenerator.buildTestHtmlContent`(**非** `generateTestReportStreaming`,后者是备选流式路径,默认不调用)。 |
| 29 | + | ||
| 30 | +## 2. 硬约束(违反即 verify 失败或安全漏洞) | ||
| 31 | + | ||
| 32 | +### R1. HTML 转义 — 所有用户/测试派生字符串必须 `escapeHtml` | ||
| 33 | +- 测试派生字段(caseName / caseId / e.name / e.details / errors / notices / logs / automationNote / actualValue / expectedValue / knownIssue)插值进 HTML 时**必须** `escapeHtml(...)`。 | ||
| 34 | +- `formatValue()` 返回**原始字符串**(用于非 HTML 的分类 reason),HTML 站点必须包 `escapeHtml(this.formatValue(x))`。 | ||
| 35 | +- 控制词汇(module / platform / status 枚举)可豁免。 | ||
| 36 | +- 任何 `onclick="document.getElementById('${id}')"` 中的 id 用 `sanitizeHtmlId` 清洗(仅 `\w\-`),同时用于 `id="..."` 与 `getElementById`,保证匹配。 | ||
| 37 | +- 反例:`<td>${r.caseName}</td>`、`title="${e.knownIssue}"`、`onclick="...('${anchorId}')"` 裸插值 → 含 `<`/`"`/`'` 即破结构/XSS。 | ||
| 38 | + | ||
| 39 | +### R2. Markdown 表格单元必须 `escapeMdCell` | ||
| 40 | +- 表格行 `| ${a} | ${b} |` 中每个用户单元用 `escapeMdCell`(转义 `|`→`\|`、换行→空格、`` ` ``→`` \` ``)。 | ||
| 41 | +- 标题/列表项(`###`、`-`)至少转义换行。 | ||
| 42 | +- `report-utils.ts` 提供 `escapeHtml` / `escapeMdCell` / `sanitizeHtmlId`,两生成器均 import 使用。 | ||
| 43 | + | ||
| 44 | +### R3. 截图路径 — 相对报告根,禁止剥 basename | ||
| 45 | +- 截图存于 `<timestampDir>/<platform>/screenshots/<name>.png`,HTML 在 `<timestampDir>/test-report.html`。 | ||
| 46 | +- result.screenshots 在 push 前转成**相对 timestampDir**:`path.relative(this.timestampDir, s).replace(/\\/g,'/')`(得 `android/screenshots/foo.png`),非 `path.basename(s)`。 | ||
| 47 | +- `buildScreenshotsHtml` 对绝对路径计算相对路径、对相对路径直接用;basename 剥离会绕过该逻辑导致 `src="foo.png"` 404。 | ||
| 48 | +- 报告生成**之后**的 slim-down(ComparisonRunner 内存)才允许 basename(已落盘不影响)。 | ||
| 49 | + | ||
| 50 | +### R4. 指标单一真相源 — 全走 `computeUnifiedMetrics` | ||
| 51 | +- **TestReport 与 ComparisonReport 都必须挂 `unifiedMetrics`**。ComparisonReport 由 `comparison-runner` 用 `computeUnifiedMetrics([...harmony, ...android], qualityReport, timestampDir)` 计算。 | ||
| 52 | +- 所有报告出口(HTML/MD/JSON/Email)的 expect 统计(totalExpects/passedExpects/knownIssueExpects/limitedExpects/effectivePassedExpects)**只读** `unifiedMetrics`,fallback 顺序 `unifiedMetrics ?? qr ?? results.reduce`(保证口径一致、同 ui 过滤)。 | ||
| 53 | +- **禁止** totalExpects 取 unifiedMetrics 而 passedExpects 取 qr 的混源(曾导致 417/364 通过>总数)。 | ||
| 54 | +- `UnifiedMetrics` 类型在 `base/types`,`computeUnifiedMetrics` 在 `metrics-computer.ts`。 | ||
| 55 | + | ||
| 56 | +### R5. 指标匹配 — 禁子串、禁 `includes(维度名)` | ||
| 57 | +- 丰富度(AR)匹配用**词边界正则**:`new RegExp('(?:^|[^a-z])(' + types.join('|') + ')(?:[^a-z]|$)','i')`,禁止 `e.details.includes('text')`("textColor" 误命中 "text")。 | ||
| 58 | +- 维度查找用 `QualityDimension.key` 字段(如 `key:'combinationCoverage'`),`find(d => d.key === 'combinationCoverage')`;`d.name.includes('组合覆盖')` 仅作兜底,不作为主键。 | ||
| 59 | +- 权重按 METRICS-SPEC §9.2 固定值:双平台 Harmony/Android 各 20%;**三平台各 15% + iOS 10%**(非等比 redistribute 缩放)。`redistributeWeights` 仅作浮点归一安全网,total=1.0 时为 no-op。 | ||
| 60 | + | ||
| 61 | +### R6. 过滤计数器 — 数唯一用例,非元素 | ||
| 62 | +- 过滤栏 `data-filter-target="#cases-table tbody tr, .detail-card"` 同时锁表格行+详情卡,`tf-total` 必须只数 `.detail-card`(= 用例数),不能数全部元素(曾显示 218/218 = 109 行 + 109 卡)。 | ||
| 63 | +- `applyFilter` 中 `if (ok && row.classList.contains('detail-card')) shown++`。 | ||
| 64 | + | ||
| 65 | +### R7. 渲染去重与采样 | ||
| 66 | +- 趋势图 HTML 在 `buildTestHtmlContent` 顶部**计算一次**存局部变量(`const trendChartsHtml = this.buildTrendChartsHtml(...)`),nav 检查/section 检查/渲染三处复用(曾 3 遍 SVG 计算)。 | ||
| 67 | +- 趋势表 `buildTrendTableRows` 必须**采样**(`data.length > 200 ? sampleData(data,200) : data`),与 sparkline 一致;禁止数千行全量渲染。 | ||
| 68 | +- 对比报告 HTML 默认走**流式**(`generateHtmlStreaming`),非全量字符串拼接(峰值内存 -10~20MB)。 | ||
| 69 | + | ||
| 70 | +### R8. 配色 — 浅色主题 + 对比度合规 | ||
| 71 | +- 报告为**浅色主题**:页面 bg `#eef2f7`,外层卡白 `#ffffff`,内层卡极浅蓝 `#f5f8fc`,边框 `#e2e8f0`/`#cbd5e1`,强调蓝 `#2563eb`(blue-600)。 | ||
| 72 | +- 文字深 slate:primary `#0f172a`、secondary `#334155`、muted `#64748b`。**蓝色仅做强调**(边框/左条/徽章/链接),卡背景用极浅蓝保证深字高对比(≥11:1)。 | ||
| 73 | +- 状态色用深变体适配浅底:success `#16a34a`、danger `#dc2626`、warning `#d97706`;徽章 bg 用 `*Light`(`#dcfce7`/`#fee2e2`/`#fef3c7`)。 | ||
| 74 | +- 蓝色按钮(如 `.back-top`)白字保留 `color:#fff`(蓝底白字合规)。 | ||
| 75 | +- 所有颜色经 `report-theme.ts` / CSS 变量,禁止内联硬编码深色魔法值(`#0f0c29`/`#06070d`/`rgba(255,255,255,0.0X)` 已清除)。 | ||
| 76 | + | ||
| 77 | +### R9. God class 拆分 | ||
| 78 | +- 单文件 ≤ 上表上限。超限按关注点拆:SVG 图表 → `chart-renderer.ts`(纯函数);设备 map 构建 → 单一 `buildDeviceMaps` helper(禁止 3 份复制)。 | ||
| 79 | +- 模板渲染与文件 I/O 分离(参考 `cptf-refactor-rules §6`)。 | ||
| 80 | +- 对比生成器(1421 行)下一步:HTML/MD/JSON 三格式拆独立类或 mixin,共享 `buildDeviceMaps`/`getDeviceInfoCompleteness`/`formatFailedStepLog`。 | ||
| 81 | +- expect-executor.ts(1004) / test-scene-navigator.ts(972) 为非报告层 God class,单独排期。 | ||
| 82 | + | ||
| 83 | +## 3. 可复用助手(`report-utils.ts` + `chart-renderer.ts` + `metrics-computer.ts`) | ||
| 20 | 84 | ||
| 21 | ```typescript | 85 | ```typescript |
| 22 | -import { ReportService } from 'cptf/internal'; | 86 | +import { escapeHtml, escapeMdCell, sanitizeHtmlId, formatMemory, formatDuration } from './report-utils'; |
| 23 | - | 87 | +import { buildSvgChart, buildSparklineSvg, buildTrendTableRows, sampleData, niceScale, computeTrendStats, lightenColor } from './chart-renderer'; |
| 24 | -const service = new ReportService({ logger }); | 88 | +import { computeUnifiedMetrics, buildQualityReport, buildComparisonQualityReport } from './metrics-computer'; |
| 25 | -const paths = await service.generateTestReports(report, outputDir, 'all'); | ||
| 26 | -await service.shutdown(); | ||
| 27 | ``` | 89 | ``` |
| 28 | 90 | ||
| 29 | -Off-thread generation in v0.2.0 prevents report generation from blocking test execution. | 91 | +- `escapeHtml(s)` — & < > " ' 全转义。`escapeMdCell(s)` — 表格单元安全。`sanitizeHtmlId(s)` — HTML id 清洗。 |
| 92 | +- 图表纯函数无 `this` 状态,仅依赖 `REPORT_THEME`;编排方法(`buildTrendChartsHtml`/`buildPerformanceTrendHtml`)留在 `html-generator`,调用纯函数 + 自有 `buildScreenshotsHtml`/`formatValue`。 | ||
| 30 | 93 | ||
| 31 | -## ReportJobQueue (Worker Threads) | 94 | +## 4. ReportService / ReportJobQueue |
| 32 | 95 | ||
| 33 | -`ReportJobQueue` schedules jobs to `JobQueue<T,R>` which dispatches to a worker thread pool. | 96 | +- `ReportService` 批量多格式经 `ReportJobQueue`(worker_threads 池)并发生成;`CPTF_ASYNC_REPORTS=false` 回退 inline。 |
| 97 | +- `defaultRegistry` 注册 `test-html/json/markdown` + `comparison-html/json/markdown`;自定义格式 `defaultRegistry.register(type, gen)`(worker 侧需单独注册,主线程注册不自动可见)。 | ||
| 98 | +- `ReportJobQueue`:`jobTimeoutMs`(120s) 与 `workerInitTimeoutMs`(10s) 分离;大 payload 不再 10s 超时 inline 重跑。 | ||
| 34 | 99 | ||
| 35 | -```typescript | 100 | +## 5. 趋势数据 |
| 36 | -export type ReportExecutionMode = 'inline' | 'worker' | 'auto'; | ||
| 37 | 101 | ||
| 38 | -const queue = new ReportJobQueue({ mode: 'auto', maxConcurrency: 4 }); | 102 | +- `TestRunner` 产 3 全局序列:`globalMemoryTrend`(KB→MB) / `globalFpsTrend` / `globalCpuTrend`,capped 10k(CompressMetrics)。 |
| 39 | -queue.enqueue({ type: 'test-html', jobId: '...', outputPath, data: report }); | 103 | +- `buildTrendPoints<T>()` 静态助手去重 sort+map。 |
| 40 | -``` | 104 | +- HTML 内联 SVG(经 `chart-renderer` 纯函数);Markdown 不含图。 |
| 41 | 105 | ||
| 42 | -## Trend Points | 106 | +## 6. 输出位置 |
| 43 | 107 | ||
| 44 | -`TestRunner` emits 3 global trend series: | 108 | +| 文件 | 路径 | |
| 45 | -- `globalMemoryTrend` — Memory (KB → MB) over time | ||
| 46 | -- `globalFpsTrend` — FPS over time | ||
| 47 | -- `globalCpuTrend` — CPU % over time | ||
| 48 | - | ||
| 49 | -`HtmlReportGenerator` inlines these as inline SVG charts. | ||
| 50 | - | ||
| 51 | -`buildTrendPoints<T>()` (static helper, v0.2.0) — eliminates 3× duplicated sort+map. | ||
| 52 | - | ||
| 53 | -## Roadmap: Custom Report Registration | ||
| 54 | - | ||
| 55 | -v0.3.0 plan (per `PLUGIN-GUIDE.md §5`): | ||
| 56 | - | ||
| 57 | -```typescript | ||
| 58 | -export interface ReportGenerator<TData> { | ||
| 59 | - format: string; | ||
| 60 | - generate(data: TData, outputPath: string, options?: unknown): Promise<ReportJobResult>; | ||
| 61 | -} | ||
| 62 | - | ||
| 63 | -ReportService.register('junit-xml', new JUnitXmlReportGenerator()); | ||
| 64 | -``` | ||
| 65 | - | ||
| 66 | -## JUnit XML Example (CI integration) | ||
| 67 | - | ||
| 68 | -```typescript | ||
| 69 | -import type { TestReportGenerator, ReportJobResult } from 'cptf/internal'; | ||
| 70 | - | ||
| 71 | -export class JUnitXmlReportGenerator implements TestReportGenerator { | ||
| 72 | - format = 'junit-xml'; | ||
| 73 | - | ||
| 74 | - async generate(report: TestReport, outputPath: string): Promise<ReportJobResult> { | ||
| 75 | - const testsuites = report.results.map(r => this.toTestSuite(r)); | ||
| 76 | - const xml = `<?xml version="1.0" encoding="UTF-8"?>\n${testsuites.join('\n')}\n`; | ||
| 77 | - await fs.promises.writeFile(outputPath, xml, 'utf8'); | ||
| 78 | - return { /* ReportJobResult */ }; | ||
| 79 | - } | ||
| 80 | - | ||
| 81 | - private toTestSuite(r: TestResult): string { | ||
| 82 | - const failures = r.success ? 0 : 1; | ||
| 83 | - return `<testsuite name="${r.caseId}" tests="1" failures="${failures}" time="${r.duration / 1000}"> | ||
| 84 | - <testcase name="${r.caseName}" classname="${r.module}"/> | ||
| 85 | - ${r.success ? '' : `<failure>${r.errors.join('; ')}</failure>`} | ||
| 86 | -</testsuite>`; | ||
| 87 | - } | ||
| 88 | -} | ||
| 89 | -``` | ||
| 90 | - | ||
| 91 | -## Output Locations | ||
| 92 | - | ||
| 93 | -| File | Path | | ||
| 94 | |------|------| | 109 | |------|------| |
| 95 | -| HTML | `test-reports/yyyy-MM-ddTHH-mm-ss/test-report.html` | | 110 | +| HTML | `test-reports/<ts>/test-report.html` | |
| 96 | | Markdown | `.../test-report.md` | | 111 | | Markdown | `.../test-report.md` | |
| 97 | | JSON | `.../test-report.json` | | 112 | | JSON | `.../test-report.json` | |
| 113 | +| 截图 | `.../<platform>/screenshots/*.png` | | ||
| 98 | | Crash logs | `.../<platform>/crash-logs/crash-<ts>.log` | | 114 | | Crash logs | `.../<platform>/crash-logs/crash-<ts>.log` | |
| 99 | -| Quality report | bundled inside `test-report.html` | | 115 | +| 用例 JSON | `.../<platform>/case-results/<caseId>.json` | |
| 116 | +| 对比报告 | `.../comparison-report.{html,md,json}` | | ||
| 100 | 117 | ||
| 101 | -## Performance Tips | 118 | +## 7. 自定义报告示例(CI 集成,含正确转义) |
| 102 | 119 | ||
| 103 | -- `CPTF_ASYNC_REPORTS=true` (default) — off-thread generation | 120 | +```typescript |
| 104 | -- `ReportFormat = 'all'` — 3 formats in parallel | 121 | +import type { ReportGenerator, ReportJobResult } from 'cptf/internal'; |
| 105 | -- `ReportJobQueue` concurrency default = CPU count | 122 | +import { escapeHtml } from 'cptf/internal'; |
| 106 | -- `globalMemoryTrend` capped at 10k samples (CompressMetrics) | ||
| 107 | 123 | ||
| 108 | -## Related Skills | 124 | +export class JUnitXmlReportGenerator implements ReportGenerator { |
| 125 | + type = 'junit-xml'; format = 'junit-xml'; fileExtension = 'xml'; scope = 'test'; | ||
| 126 | + generate(data, outputPath) { | ||
| 127 | + const report = data as any; | ||
| 128 | + const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<testsuites>\n${ | ||
| 129 | + report.results.map((r: any) => `<testsuite name="${escapeHtml(r.caseId)}" tests="1" failures="${r.success ? 0 : 1}" time="${r.duration / 1000}"><testcase name="${escapeHtml(r.caseName)}" classname="${escapeHtml(r.module)}"/>${r.success ? '' : `<failure>${escapeHtml(r.errors.join('; '))}</failure>`}</testsuite>`).join('\n') | ||
| 130 | + }\n</testsuites>`; | ||
| 131 | + return { success: true, outputPath, durationMs: 0, sizeBytes: Buffer.byteLength(xml), jobId: '' }; | ||
| 132 | + } | ||
| 133 | +} | ||
| 134 | +``` | ||
| 135 | +注意:XML 属性值同样必须 `escapeHtml`(`"` → `"`)。 | ||
| 109 | 136 | ||
| 110 | -- `cptf-cli` — CLI invocation | 137 | +## 8. 反模式黑名单 |
| 111 | -- `cptf-test-api` — Test config | 138 | + |
| 112 | -- `cptf-arch` — Architecture | 139 | +| 反模式 | 后果 | 正解 | |
| 140 | +|--------|------|------| | ||
| 141 | +| `${caseName}` 裸插值 HTML | `<`/`"` 破结构、XSS | `escapeHtml(caseName)` | | ||
| 142 | +| `path.basename(screenshot)` push 前 | 截图 404 不显示 | `path.relative(timestampDir, s)` | | ||
| 143 | +| totalExpects 取 unifiedMetrics、passedExpects 取 qr | 417/364 通过>总数 | 全取 unifiedMetrics | | ||
| 144 | +| `e.details.includes('text')` | "textColor" 误算富断言 | 词边界正则 | | ||
| 145 | +| `d.name.includes('组合覆盖')` 查维度 | 改名即失效 | `d.key === 'combinationCoverage'` | | ||
| 146 | +| 三平台 `redistributeWeights` 等比缩放 | 权重偏离 spec §9.2 | Harmony/Android 0.15、iOS 0.10 | | ||
| 147 | +| `buildTrendChartsHtml` 调 3 次 | SVG 算 3 遍 | 顶部缓存局部变量 | | ||
| 148 | +| 趋势表全量渲染 | 数千行 DOM | `sampleData(data,200)` | | ||
| 149 | +| 对比报告全量字符串拼接 | 峰值内存 +10~20MB | 默认 `generateHtmlStreaming` | | ||
| 150 | +| 设备 map 循环复制 3 份 | 维护负担 | 单 `buildDeviceMaps` helper | | ||
| 151 | +| 内联 SVG ~465 行塞 html-generator | God class | 抽 `chart-renderer.ts` 纯函数 | | ||
| 152 | +| 过滤计数器数元素非用例 | 218/218 误显 | 只数 `.detail-card` | | ||
| 153 | +| 内联硬编码深色魔法值 | 主题不可控 | 全走 theme 变量 | | ||
| 154 | + | ||
| 155 | +## 相关 Skill | ||
| 156 | + | ||
| 157 | +- `cptf-cli` — CLI 调用 | ||
| 158 | +- `cptf-test-api` — 用例/Step 配置 | ||
| 159 | +- `cptf-arch` — 五层架构 | ||
| 160 | +- `cptf-refactor-rules` — God class 拆分 / mixin / 模板-IO 分离 | ||
| 161 | +- `cptf-optimization-verification` — 静态分析误报识别 | ||
| @@ -162,8 +162,6 @@ export interface TestConfig { | |||
| 162 | scene?: string; | 162 | scene?: string; |
| 163 | category?: string; | 163 | category?: string; |
| 164 | sections?: string[]; | 164 | sections?: string[]; |
| 165 | - scrollTarget?: string; | ||
| 166 | - scrollTargetRequired?: boolean; | ||
| 167 | 165 | ||
| 168 | captureScreenshots?: boolean; | 166 | captureScreenshots?: boolean; |
| 169 | waitAfterAction?: number; | 167 | waitAfterAction?: number; |
| @@ -141,8 +141,286 @@ cptf scan -m stability # list cases without running | |||
| 141 | cptf run -p harmony -t button-onpress --output ./debug-run | 141 | cptf run -p harmony -t button-onpress --output ./debug-run |
| 142 | ``` | 142 | ``` |
| 143 | 143 | ||
| 144 | +## WSL2 + HarmonyOS 设备调试陷阱(实战总结) | ||
| 145 | + | ||
| 146 | +### USB/IP 透传方案已废弃 | ||
| 147 | + | ||
| 148 | +**问题**: usbipd-win 将 USB 设备透传到 WSL2 在 ADB/HDC 场景下极不稳定: | ||
| 149 | +- 复合 USB 设备(如 Mate 60 Pro: ADB+MTP+HDC)attach 后 0.008 秒断连 | ||
| 150 | +- `adb restart-server` / `hdc kill` 触发 USB 重新枚举,破坏虚拟总线绑定 | ||
| 151 | +- Windows 宿主机 adb.exe/hdc.exe 抢占设备控制权 | ||
| 152 | + | ||
| 153 | +**正确方案**: TCP 端口共享(Windows 原生管理 USB,WSL2 通过 TCP 连接) | ||
| 154 | +``` | ||
| 155 | +WSL2 adb/hdc client → TCP 127.0.0.1:5037/8710 → Windows adb.exe/hdc.exe server → USB → 设备 | ||
| 156 | +``` | ||
| 157 | + | ||
| 158 | +### Mirrored 网络模式特性 | ||
| 159 | + | ||
| 160 | +| 特性 | NAT 模式 | Mirrored 模式 | | ||
| 161 | +|------|----------|---------------| | ||
| 162 | +| 127.0.0.1 | 独立子网 | **共享 loopback**(WSL ↔ Windows 互通) | | ||
| 163 | +| 未监听端口 connect | 立即 ECONNREFUSED | **永久 hang**(不返回错误) | | ||
| 164 | +| adb start-server | 正常 | **卡死**(先 connect 检查 → hang) | | ||
| 165 | +| 代理 127.0.0.1:port | 正常 | **连接失败**(走 Windows 网络栈) | | ||
| 166 | + | ||
| 167 | +**修复 adb hang**: 用 `nc -z -w1` 检测端口是否监听,避免直接 connect。未监听时后台启动 `adb.exe -a -P 5037 nodaemon server`。 | ||
| 168 | + | ||
| 169 | +**修复代理连接失败**: `no_proxy` 加入内网域名 `.openharmony.cn,.huawei.com,.huaweicloud.com`。 | ||
| 170 | + | ||
| 171 | +### HDC 设备 Offline 排查 | ||
| 172 | + | ||
| 173 | +| 原因 | 症状 | 修复 | | ||
| 174 | +|------|------|------| | ||
| 175 | +| 手机未授权 USB 调试 | `Offline`,序列号为 `0123456789ABCDEF` | 手机设置 → 开发者选项 → USB 调试 → 授权弹窗点「允许」 | | ||
| 176 | +| usbipd bind 后 Windows adb 看不到 | bind 接管设备给 USB/IP | **unbind** 让 Windows adb 直连 | | ||
| 177 | +| 系统固件 Beta 版 | `uitest start-daemon` 报 `Failed to get apiCaller` | Beta2 不暴露 testserver SA 5502 apiCaller,需正式版固件 | | ||
| 178 | + | ||
| 179 | +### Native Driver (ohosTest) 架构 | ||
| 180 | + | ||
| 181 | +**核心原则**: `Driver.create()` 必须在 ohosTest 进程内调用,不在应用进程内。 | ||
| 182 | + | ||
| 183 | +``` | ||
| 184 | +错误(应用进程内): EntryAbility → Driver.create() → 与 RNOH 渲染竞争 → 失败 | ||
| 185 | +正确(ohosTest 进程): aa test → ohosTest 进程 → Driver.create() → delegator.startAbility(EntryAbility) → 正常 | ||
| 186 | +``` | ||
| 187 | + | ||
| 188 | +**aa test 命令格式**: | ||
| 189 | +```bash | ||
| 190 | +aa test -b <bundleName> -m entry_test -s unittest OpenHarmonyTestRunner -s timeout 600000 -w 600 | ||
| 191 | +``` | ||
| 192 | + | ||
| 193 | +**WebSocket 连接方案**: `hdc rport tcp:18081 tcp:18081`(反向端口转发),设备端 WS client → `ws://127.0.0.1:18081` → USB → 宿主机 WS Server。 | ||
| 194 | + | ||
| 195 | +**HAR 包依赖**: ArkTS 源码不能通过 `file:` 目录引用(hvigor OHM 解析器报 `Cannot import files outside of the current module`),必须打包为 `.har` 文件引用。 | ||
| 196 | + | ||
| 197 | +### ArkTS 严格模式常见错误 | ||
| 198 | + | ||
| 199 | +| 错误 | 原因 | 修复 | | ||
| 200 | +|------|------|------| | ||
| 201 | +| `arkts-no-any-unknown` | 用了 `any`/`unknown` 类型 | 改为显式类型 `number`/`string`/`Record<string, Object>` | | ||
| 202 | +| `arkts-no-untyped-obj-literals` | `{ key: value }` 无类型 | 先声明 `const r: Record<string, Object> = {}` 再 `r['key'] = value` | | ||
| 203 | +| `arkts-no-decl-merging` | 同名 interface 定义两次 | 删除重复定义 | | ||
| 204 | +| `arkts-limited-throw` | `throw err`(err 是 unknown) | `throw new Error(msg)` | | ||
| 205 | +| `Cannot find module '@ohos/hilog'` | ohosTest 用旧 import | 改为 `import { hilog } from '@kit.PerformanceAnalysisKit'` | | ||
| 206 | +| `On.id()` 编译报错 | `On` 是 class 不是 namespace | 用 `new On()` 或 `On as Object as Record<string, Object>` 动态调用 | | ||
| 207 | +| `hdc.exe` 在 systemd service 中启动失败 | systemd 无法管理 Windows .exe | 用 `dev-init.sh` fallback(nc 检测 + nohup 启动) | | ||
| 208 | + | ||
| 209 | +### 测试报告 screenCap 路径问题 | ||
| 210 | + | ||
| 211 | +ohosTest 进程中 `Driver.screenCap()` 在某些固件上拒绝 `/data/local/tmp/` 路径。`dumpLayout` 写 `/data/local/tmp/` 正常但 `screenCap` 不行——这是 Driver API 自身限制,不是权限问题。 | ||
| 212 | + | ||
| 213 | +### 构建优化要点 | ||
| 214 | + | ||
| 215 | +| 问题 | 修复 | | ||
| 216 | +|------|------| | ||
| 217 | +| `tsconfig.build.json` 含 `sourceMap: true` | 改为 `false`,发布包不需要 source map | | ||
| 218 | +| `tsconfig.build.json` 含 `declarationMap: true` | 改为 `false` | | ||
| 219 | +| dist 包含 `__tests__/` | `exclude` 加 `"src/**/__tests__/**"` | | ||
| 220 | +| 循环导入 `enhanced-error-handler ↔ error-patterns` | 共享类型定义移到被依赖方 | | ||
| 221 | +| `exec(cmd, { timeout: 0 }, () => {})` | 设 `timeout: 600000` + 回调记录退出日志 | | ||
| 222 | +| fire-and-forget `.catch()` | 改为 `await` + try/catch | | ||
| 223 | +| `as string`/`as boolean` 强转 | 改用 `String()`/`Boolean()` 安全转换 | | ||
| 224 | +| `Promise<any>` / `args: any[]` | 改为 `Promise<unknown>` / `args: unknown[]` | | ||
| 225 | + | ||
| 144 | ## Related Skills | 226 | ## Related Skills |
| 145 | 227 | ||
| 146 | - `cptf-test-api` — Test case writing | 228 | - `cptf-test-api` — Test case writing |
| 147 | - `cptf-cli` — Running tests | 229 | - `cptf-cli` — Running tests |
| 148 | - `cptf-troubleshoot` (this skill) — Debugging | 230 | - `cptf-troubleshoot` (this skill) — Debugging |
| 231 | + | ||
| 232 | + | ||
| 233 | +## Architecture Changes (v0.3.0+) | ||
| 234 | + | ||
| 235 | +### DeviceBridge Layer Removed | ||
| 236 | + | ||
| 237 | +**What changed:** | ||
| 238 | +- `DeviceBridge` / `NativeDeviceBridge` / `ExternalDeviceBridge` / `DeviceBridgeFactory` → DELETED | ||
| 239 | +- `UIStrategy` / `ExternalUIStrategy` / `NativeUIStrategy` → DELETED | ||
| 240 | +- `UIAutomation` now delegates directly to `ISystem` (no intermediate layer) | ||
| 241 | + | ||
| 242 | +**New architecture:** | ||
| 243 | +``` | ||
| 244 | +用例 → UIAutomation → ISystem (clickByTestId etc.) | ||
| 245 | + ├── native: HarmonyNativeDriver.clickComponent() (1 WS) | ||
| 246 | + └── external: getElementByTestId → click(x,y) (shell) | ||
| 247 | +``` | ||
| 248 | + | ||
| 249 | +**If you see DeviceBridge references:** | ||
| 250 | +DeviceBridge / NativeDeviceBridge / ExternalDeviceBridge / DeviceBridgeFactory 已于 v0.3.0 全部删除。`grep -rn "DeviceBridge\|getDeviceBridge\|deviceBridge" packages/framework/src/` 应返回 0 结果。如果发现新引用,说明引入了已删除的 API,需改为: | ||
| 251 | +- 旧的 `bridge.findElement()` → `system.getElementByTestId()` 或 `UIAutomation.findByTestId()` | ||
| 252 | +- 旧的 `bridge.click(x, y)` → `system.click(x, y)` 或 `UIAutomation.click(x, y)` | ||
| 253 | +- 旧的 `bridge.dumpLayout()` → `system.getUIElements()` 或 `UIAutomation.getLayout()` | ||
| 254 | + | ||
| 255 | +### 已完成的清理(v0.3.0) | ||
| 256 | + | ||
| 257 | +以下清理在 v0.3.0 已完成,无需再处理: | ||
| 258 | +- ✅ `scroll-automator.ts`:DeviceBridge import + bridge 字段已移除 | ||
| 259 | +- ✅ `metric-collectors.ts`:DeviceBridge import + bridge params 已移除 | ||
| 260 | +- ✅ `test-scene-navigator.ts`:6x `getDeviceBridge()` 替换为 `system.switchCase()` | ||
| 261 | +- ✅ `execution-engine.ts`:2x `getDeviceBridge()` 替换为 `system.switchCase()` | ||
| 262 | +- ✅ `ui-automation.ts`:`getDeviceBridge(): any` compat 方法已删除 | ||
| 263 | + | ||
| 264 | +### Native Driver Action List (NativeTestDriver.ets) | ||
| 265 | + | ||
| 266 | +25+ actions exposed over WebSocket: | ||
| 267 | +- Coordinate: click, doubleClick, longClick, swipe, drag | ||
| 268 | +- Component-direct: clickComponent, inputTextComponent, clearTextComponent, scrollToTop, scrollToBottom | ||
| 269 | +- Native: waitForComponent, assertComponentExist, findComponents, getComponentState | ||
| 270 | +- Gesture: dragTo, pinchOut, pinchIn, scrollSearch | ||
| 271 | +- Display: getDisplaySize, getDisplayRotation, setDisplayRotation | ||
| 272 | +- Keys: triggerKey, pressBack, pressHome, wakeUpDisplay | ||
| 273 | +- Layout: dumpLayout, screenCap, findComponent | ||
| 274 | +- Utility: delayMs, waitForIdle, ping | ||
| 275 | + | ||
| 276 | +--- | ||
| 277 | + | ||
| 278 | +## 环境与长跑踩坑(2026-08 全量验证实测) | ||
| 279 | + | ||
| 280 | +### Gradle 外部 srcDir 不重编(Android native 静默旧代码) | ||
| 281 | + | ||
| 282 | +`app/build.gradle` 的 `sourceSets.androidTest.java.srcDirs += ['../../../cptf/packages/native-android/...']` 指向 workspace 外部目录时,**改 Java 文件后 `assembleAndroidTest` 报 BUILD SUCCESSFUL in 1-2s 但 dex 未更新**(up-to-date 检查对跨工程外部路径失效)。表象:改了 DeviceDriver.java 行为不变 / apk 里 strings 查不到新方法名。 | ||
| 283 | +**验证方法**:`unzip -p apk classes*.dex | strings | grep <新方法名>`(注意多 dex,classes.dex 可能不含目标类)。 | ||
| 284 | +**修复**:改完后跑 `./gradlew :app:cleanPackageDebugAndroidTest :app:assembleAndroidTest`,并用 strings 验证新符号真的进了 dex。**凡是"改了设备端 Java 但日志行为没变",先怀疑这个**。 | ||
| 285 | + | ||
| 286 | +### adb hang / 设备掉线的恢复序列 | ||
| 287 | + | ||
| 288 | +USB 长跑中 adb server 会卡死(`adb devices` 超时 exit=124,5037 无监听)。恢复顺序: | ||
| 289 | +1. `timeout 12 adb devices` 确认(**永远带 timeout**,裸跑会挂死整个 shell) | ||
| 290 | +2. 卡死时 `pkill -9 -f adb` + 等设备物理重连(USB 掉线无软件解法) | ||
| 291 | +3. 反复 `adb kill-server && adb start-server` 无效且可能加剧卡死——server hang 时优先 pkill | ||
| 292 | +4. 与 HarmonyOS 设备并行测试时,**adb/hdc/18081-18083 端口会被 harmony 进程挤占**,android native connect timeout 前先 `ss -tlnp | grep 1808` 检查是谁占的 | ||
| 293 | + | ||
| 294 | +### 单进程全量 functional 必 OOM(16G 主机) | ||
| 295 | + | ||
| 296 | +815 用例单进程跑 ~40min 后 node(--max-old-space-size=16384)被内核 OOM kill,表象:**exit=0 但 log 戛然而止、无总通过/失败汇总、case-results 缺失**(pnpm 把 SIGKILL 吞成 0)。 | ||
| 297 | +**修复模式:分批跑**——每批 ~15 用例独立进程(`pnpm test -t id1 id2 ...`),批间 sleep 让内存归零。116 采样用例 8 批全程零中断。**"exit=0 但没有汇总行" = 进程被杀,不要当成功**。 | ||
| 298 | + | ||
| 299 | +### bash 工具跑长任务的三档后台方案(实测可靠度递增) | ||
| 300 | + | ||
| 301 | +| 方案 | 结果 | 结论 | | ||
| 302 | +|---|---|---| | ||
| 303 | +| `命令 &` / `nohup ... &` | bash 会话结束时被杀 | 不可用 | | ||
| 304 | +| `screen -dmS` | 偶尔存活,会被会话清理连带杀 | 不稳定 | | ||
| 305 | +| **`nohup setsid bash -c '...' < /dev/null > /dev/null 2>&1 & disown`** | 全程存活 | ✅ 唯一可靠 | | ||
| 306 | + | ||
| 307 | +前台直接跑受 shell 工具 timeout 限制(超时即 SIGKILL,"Killed" 进 log)。**长任务一律 setsid 后台 + 轮询 log 文件**。轮询命令本身也别裸跑 adb。 | ||
| 308 | + | ||
| 309 | +### native 端口漂移(connect 失败 → 18081→18082→18083) | ||
| 310 | + | ||
| 311 | +PortManager 是**进程内单例**:connect 失败不 release 时,Map 记录泄漏,同进程重试拿到 18082,但 18082 的 adb reverse 未建立 → instrumentation 永远连不上(表象 `connect timeout on port 18082`)。 | ||
| 312 | +**规则**:任何 `pm.allocate()` 的失败路径必须 `pm.release(port)` + 关 WS server + 清 `_actualPort`(android-native-driver / harmony-native-driver 的 connect catch 已修,新平台 driver 复制该模式)。 | ||
| 313 | + | ||
| 314 | +### watchdog 低估导致 5 迭代用例必然超时 | ||
| 315 | + | ||
| 316 | +旧公式 `estimatedMs = executeTimes × max(cooldownMs, 1500) + 30s`:cooldown=2000 → 5×2+30=40s 估算,实际单迭代 ~28s → 真实 ~170s > watchdog 120s → **稳定在第 3-4 迭代被杀**。 | ||
| 317 | +新公式(case-parser.ts):解析用例源码里的 `this.ui?.sleep(N)` 累加为 stepSleepMs,`perIterTotal = max(stepSleepMs + cooldown, 15s)`,`(executeTimes) × perIterTotal + 30s`。**新增性能用例若仍超时,先查估算是否覆盖:步骤 sleep + 导航 + 指标等待 + warmup**。 | ||
| 318 | + | ||
| 319 | +### metric 事件总线协议(D3 修复后的接线全景) | ||
| 320 | + | ||
| 321 | +``` | ||
| 322 | +设备端推送 ─┬─ MetricWebSocketServer(external) ── test-runner 桥接 ──┐ | ||
| 323 | + └─ nativeDriver WS(native) ── subscribeNativeMetricStream ─┤ | ||
| 324 | + ▼ | ||
| 325 | + 全局 bus(base/utils/metric-event-bus.ts, | ||
| 326 | + 复用 benchmark 的 global.__metricEventBus) | ||
| 327 | + ▼ | ||
| 328 | + RenderTimeCollector.subscribe(`renderTime:${deviceId}`) | ||
| 329 | +``` | ||
| 330 | +**三路分流规则**(performance-collector.subscribeNativeMetricStream):`renderTime/unmountTime` → emitMetricEvent(bus);`type:'metric'`(设备端批量 fps/pss/cpu 格式)→ **只进 latestMetric 缓存,不进 metricsHistory**(否则全 0 脏数据污染 avg);`fps/memory/cpu/marker`(MetricEvent 格式)→ handleMetricEvent 进 history。deviceId 缺省由 native driver 用 config.deviceId 补齐(bus key 必须匹配)。 | ||
| 331 | + | ||
| 332 | +### forceRefresh 死参数(Strategy 拆分类重构的典型回归) | ||
| 333 | + | ||
| 334 | +重构把单类拆成 Native/External 子类时,基类缓存守卫代码被复制进两个子类,`if (!forceRefresh) return cached;` 后面**多带了一行 `return cached;`**(两分支等价)或穿透分支末尾保留 `return cached`——forceRefresh 从此永不生效。 | ||
| 335 | +**表象**:`waitForTestId`/`waitForElementDisappear` 轮询(R6/R7 要求 forceRefresh=true)在 cache TTL 窗口内全部读到陈旧布局 → "元素已出现但轮询超时"类 flake,且 native/external 双模式都中。 | ||
| 336 | +**守则**:① 复制缓存守卫到子类后,必须写一个"forceRefresh 必须穿透"的单测(mock fetch 计数,断言连续两次 getUIElements(true) 触发两次 fetch);② code review 缓存相关 diff 时专门盯"两个 return cached 相邻"这一模式。 | ||
| 337 | + | ||
| 338 | +### static Map 类单例的测试并发 flake(跨文件污染) | ||
| 339 | + | ||
| 340 | +`MetricCollectorFactory.customCollectors` 这类 **static Map 在同一 jest worker 内跨测试文件共享**。测试里 `register('fps', ...)` 后不清理:若后续断言先抛(unregister 行没执行)或该文件先跑完,同 worker 的其他文件(execution-engine-cleanup、extra-metrics)读到污染 factory → **间歇失败**(取决于 jest 文件调度顺序,单跑必过、全量偶发,5-10 连跑都可能复现不了)。 | ||
| 341 | +**守则**:① 测试内 register 了全局 static 注册表(MetricCollectorFactory/ReportGeneratorRegistry/PLATFORM_REGISTRY/SystemFactory)→ describe 级 `afterEach(() => Xxx.clearCustom/unregister)`;② 这类 flake 的判定法:失败用例**单跑通过** + 全量偶发 + 涉及共享注册表 = 并发污染,不要怀疑业务代码。 | ||
| 342 | + | ||
| 343 | +### 吸收外部 PR 到重构后代码的移植方法(PR#7 实战) | ||
| 344 | + | ||
| 345 | +直接 `git merge`/`cherry-pick` 一个基于**旧类结构**的 PR 到已重构分支(如 Strategy 拆分后),冲突解出的补丁极易放错类(如把 External 路径补丁放进 Native 子类)。正确流程: | ||
| 346 | +1. `git fetch <remote> +refs/merge-requests/<id>/head:pr_N` 取 PR head | ||
| 347 | +2. `git show <commit> -- <file>` 逐 commit 提取**语义补丁**(而非文本补丁) | ||
| 348 | +3. 按"PR 改的是哪条执行路径"定位重构后的落点:external 路径补丁 → External 子类;解析类补丁 → 基类(protected,两子类共用);类型补丁 → base/types | ||
| 349 | +4. 跳过与本地已等价实现的重复项(对照实现逐行比对,如 knownIssue 批次隔离) | ||
| 350 | +5. 吸收后全量验证(typecheck/tests×连跑/verify/build)+ 重读 PR 注释里的实测数据一起移植(如 KEYCODE 映射的设备验证注释) | ||
| 351 | +**评估 PR 本身**:先验证声明是否成立(对照本地代码找原始 bug 位置),不采信 PR 描述——PR#7 的 forceRefresh 声明"低估了严重性"(本地是加强版死参数),也有 PR 声称修复但本地已有等价实现(跳过)。 | ||
| 352 | + | ||
| 353 | +## Android 全量验证踩坑(2026-08-22 external/native 双模式实测) | ||
| 354 | + | ||
| 355 | +### ATX agent 死后永不自愈(936 次连续降级) | ||
| 356 | + | ||
| 357 | +**根因链**(三层叠加,缺一不可): | ||
| 358 | +1. 一次瞬时 adb 隧道超时 → layout fallback **无条件 kill 健康的 ATX agent**(防 UiAutomation 冲突的过度防御) | ||
| 359 | +2. `HealthMonitor.recordAttempt()` 在**失败请求**前也刷新 `lastActivityAt` → `isStale()` 永假 → `ensureFreshBeforeSend` 永不 ping → 永不触发重连 | ||
| 360 | +3. 死 agent 后每次 dumpHierarchy 都走 shell uiautomator fallback(+3~8s),且 fallback 每次还再 kill 一次(已是尸体) | ||
| 361 | + | ||
| 362 | +**修复**(三层防护):`recordAttempt()` 只记 `lastAttemptAt` 不动 activity;U2 client 连续 3 次请求失败 → 主动 `reconnect()`(先 probeExistingService 复用存活 agent);fallback kill 前查 `!u2.reconnecting` guard + 瞬时错误先重试 1 次(agent 存活则返回缓存布局)。 | ||
| 363 | +**教训**:健康监测的"活跃时间"只能由**成功**刷新;降级路径破坏资源前必须探测资源是否真的死了。 | ||
| 364 | + | ||
| 365 | +### u2 dumpHierarchy compressed 剔除 hint(placeholder 断言全灭) | ||
| 366 | + | ||
| 367 | +`dumpWindowHierarchy(compressed=true)` 的 XML **没有 hint 属性**(实测同一界面 compressed 0 个 / uncompressed 49 个)。placeholder/accessibilityHint 断言依赖 `dumpHierarchy(compressed=false)`。native 侧同理:`UiDevice.dumpWindowHierarchy` 是压缩 XML——自实现 AccessibilityNodeInfo 树序列化(含 `getHintText()`,API26+)。 | ||
| 368 | + | ||
| 369 | +### 华为 ROM UiAutomation 手势注入恒定 5s/事件(ALN-AL00 Android 12) | ||
| 370 | + | ||
| 371 | +`GestureController: Gestures took longer than expected (10833ms)` / 每次 `injectInputEvent` 5s(async 模式同样阻塞)。ANR log 铁证:`Waited 5001ms for MotionEvent(UP)`。主线程栈卡在 `Choreographer.doFrame → AnimationUtils.unlockAnimationClock`(ROM 动画时钟锁与注入路径竞争)。 | ||
| 372 | +**修复**:DeviceDriver 全部手势走 `Runtime.exec("input tap|swipe ...")`(shell input 命令 74ms,绕过 UiAutomation 注入路径)。**禁止** `uiDevice.click/swipe`、`UiObject2.click/scroll`、`injectInputEvent`(含 runOnMainSync 包装——主线程等注入返回、注入等主线程消费事件,直接死锁)。 | ||
| 373 | +**诊断手法**:分段计时埋点(find/bounds/tap 各多少 ms)→ 锁定 tap → 每事件计时(down/up 各 5s)→ ANR logcat 交叉定位 InputDispatcher 等待。**"恒定整数秒"耗时 = 系统/ROM 层超时,不是业务代码慢**。 | ||
| 374 | + | ||
| 375 | +### instrumentation 冷启动 dexopt 首次 >30s | ||
| 376 | + | ||
| 377 | +首轮 am instrument 触发 dexopt/AOT 校验可超 30s——连接超时必须 ≥60s,否则首轮全超时、二轮正常(假象不稳定)。 | ||
| 378 | + | ||
| 379 | +### Native 模式 ATX 必须彻底跳过 | ||
| 380 | + | ||
| 381 | +`AndroidSystem._doInitU2()` 在 `driverMode==='native'` 时直接 return(不 connect、不 spawn)。UiAutomation 全系统单例:ATX 注册失败后的残留 app_process 会干扰 DeviceDriver 的 input 注入。之前"启动失败也无妨"是错觉。 | ||
| 382 | + | ||
| 383 | +### 多窗口 dump 爆 WS maxPayload(linking-opensettings) | ||
| 384 | + | ||
| 385 | +点击"打开系统设置"后系统 Settings 窗口(巨型 a11y 树)进入 `getWindows()` 遍历 → 单帧 dump >256MB 上限 → WS 断连。**修复**:dump 按 `getTargetContext().getPackageName()` 过滤窗口 + 序列化 16MB 硬上限。**教训**:遍历系统全窗口的 API(getWindows/getRootInActiveWindow)必须按包名过滤。 | ||
| 386 | + | ||
| 387 | +### a11y 树容器合并(历史案例:49 个用例连坐失败,scrollTarget 已废弃) | ||
| 388 | + | ||
| 389 | +Android a11y 树**合并容器节点**:`View`/`ScrollView` 等无文本容器的 testID 永不出现在 dump——断言与点击只能指向**真实渲染的叶子元素**(详见 app 侧 rn-test-case skill R54)。历史根因:scrollTarget 曾指向 section 容器 → scrollSearch 永远找不到 → `场景导航失败` → 每例烧 100s。scrollTarget 字段已从框架删除,元素就绪等待由 expect 层承担(`findElementForExpect`)。 | ||
| 390 | +**注意 zsh 陷阱**:`pnpm test -- -t $IDS`(空格分隔字符串)在 zsh 不分词——必须 `${=IDS}` 或命令替换。被吞成单参时框架报"未发现测试用例",极易误判为筛选器坏了。 | ||
| 391 | + | ||
| 392 | +### CLI 堆内存 re-exec 吞退出码(全量跑双倍耗时) | ||
| 393 | + | ||
| 394 | +`src/cli/index.ts` 的 heap-boost re-exec:`execFileSync` 在子进程 exit≠0 时**抛异常**,被外层 `catch {}` 吞掉 → fall-through 到 `program.parse()` → **父进程把整套用例重跑一遍**。任何有失败用例的 run 全部双倍耗时(4234s 的 run 实际重复了两遍)。**修复**:catch 里 `process.exit(e.status ?? 1)` 传播退出码。**表象识别**:log 里出现两次"CPTF 测试执行"头 + 两份相同报告目录。 | ||
| 395 | + | ||
| 396 | +## Harmony native "元素不存在" 诊断方法论(2026-08-24 实战) | ||
| 397 | + | ||
| 398 | +### 症状特征 | ||
| 399 | + | ||
| 400 | +native 模式下 expect 全量报 `元素不存在`(每个恰好 5s = expect 层 waitForTestId 超时),但: | ||
| 401 | +- 场景容器检查通过(`Native nav: scene ready`) | ||
| 402 | +- `期望 visible=false` 的用例"通过"(元素本该存在却找不到) | ||
| 403 | + | ||
| 404 | +### 分层定位法(从快到慢) | ||
| 405 | + | ||
| 406 | +1. **区分 findComponent / dumpLayout 两条链**: | ||
| 407 | + - `findComponent`(scene 检查走的)正常 + expect 失败 → 查 UIAutomation 构造时序(findStrategy 锁定问题) | ||
| 408 | + - 探针直调 `findComponent` 正常 + `dumpLayout` 返回 `{"layout":""}` → 设备端 dump API 问题 | ||
| 409 | +2. **UI 导出数据佐证**:报告 `baseline/ui-*.json` 显示 `(0个元素)` → dumpLayout 链路断 | ||
| 410 | +3. **探针直连设备 WS**(绕过框架,一锤定音):手写 TCP+WS 握手(server→client 帧**必须不 mask**),依次调 `navigate`/`findComponent`/`dumpLayout`/`screenCap` 对比 | ||
| 411 | +4. **SDK 权威验证**:`/path/sdk/default/openharmony/ets/api/@ohos.UiTest.d.ts` 查真实签名——**不要凭记忆写 API**(dumpLayout 实际是 `(savePath, displayId?): Promise<boolean>`,不是无参返回 JSON) | ||
| 412 | + | ||
| 413 | +### 已实锤的三类根因(勿重复排查) | ||
| 414 | + | ||
| 415 | +| 根因 | 判别特征 | 修复 | | ||
| 416 | +|------|---------|------| | ||
| 417 | +| UIAutomation 构造时序(findStrategy 锁定 External) | 探针 findComponent 正常但 expect 全挂 | system 层换方案(findByTestId 直通 system.getElementByTestId → NativeLayoutProvider facade) | | ||
| 418 | +| `driver.dumpLayout()` 无参调用参数错误 | 探针 dumpLayout 返回空 + hilog 无报错(catch 静默) | 传沙箱 savePath + fileIo.readTextSync 读回 | | ||
| 419 | +| `driver.screenCap(path)` 沙箱路径限制 | `Native screenCap失败: Invalid file path:/data/local/tmp/...` | sandboxPathFor 转 tempDir + publicPathFor 转物理路径返回 host | | ||
| 420 | + | ||
| 421 | +### 探针脚本要点(自建 WS server 连 ohosTest) | ||
| 422 | + | ||
| 423 | +- `hdc rport tcp:PORT tcp:PORT` + `aa test ... -s wsPort PORT -s wsHost 127.0.0.1` | ||
| 424 | +- WS 握手:`Sec-WebSocket-Accept = base64(sha1(key + GUID))` | ||
| 425 | +- **server→client 帧禁止 mask**(client→server 必须 mask)——mask 位写反则命令全部无响应(timeout) | ||
| 426 | +- 帧解码注意握手字节残留(首次 data 含 HTTP 头,必须先剥离 `\r\n\r\n` 再解帧) | ||
| @@ -6,25 +6,30 @@ | |||
| 6 | > | 6 | > |
| 7 | > **核心准则**:[CORE-PRINCIPLES.md](CORE-PRINCIPLES.md) — 准确性(第一原则)、稳定性、执行性能。所有开发必须遵守。 | 7 | > **核心准则**:[CORE-PRINCIPLES.md](CORE-PRINCIPLES.md) — 准确性(第一原则)、稳定性、执行性能。所有开发必须遵守。 |
| 8 | > | 8 | > |
| 9 | -> **未来架构**:[DEVICE-NATIVE-ARCHITECTURE.md](packages/framework/docs/architecture/DEVICE-NATIVE-ARCHITECTURE.md) — 参照 Detox 模式的设备端原生执行架构方案。 | 9 | +> **当前架构**:[DEVICE-NATIVE-ARCHITECTURE.md](packages/framework/docs/architecture/DEVICE-NATIVE-ARCHITECTURE.md) — 双模式(native/external)架构现状 + 4 级降级策略。 |
| 10 | 10 | ||
| 11 | ## 关键约束 | 11 | ## 关键约束 |
| 12 | 12 | ||
| 13 | - **License**:MIT / gaoyang(gyange@163.com)。所有新增文件必须添加 License 头。 | 13 | - **License**:MIT / gaoyang(gyange@163.com)。所有新增文件必须添加 License 头。 |
| 14 | -- **架构**:3层分离(基础设施 → 系统 → 调度),禁止反向依赖。详见 [ARCHITECTURE-VIEWS.md](packages/framework/docs/architecture/ARCHITECTURE-VIEWS.md) 和 [ARCHITECTURE.md](packages/framework/docs/architecture/ARCHITECTURE.md)。 | 14 | +- **架构**:五层分离(编排 orchestration → 能力 capability → 系统 system → 基础设施 infrastructure → 基础 base,src/ 目录即五层;对外概览常将 base+infrastructure 合称基础设施、capability+orchestration 合称调度,共 3 组),禁止反向依赖。详见 [ARCHITECTURE-VIEWS.md](packages/framework/docs/architecture/ARCHITECTURE-VIEWS.md) 和 [ARCHITECTURE.md](packages/framework/docs/architecture/ARCHITECTURE.md)。 |
| 15 | - **接口分离**:`ISystem` 拆 7 子接口(Core/UI/App/Perf/Stability/Media/Session),新增平台实现 1 个 class 即可。 | 15 | - **接口分离**:`ISystem` 拆 7 子接口(Core/UI/App/Perf/Stability/Media/Session),新增平台实现 1 个 class 即可。 |
| 16 | - **包依赖**:`app` 通过 workspace 协议依赖 `@cptf/framework`(pnpm monorepo),改 `packages/framework` 后自动同步。 | 16 | - **包依赖**:`app` 通过 workspace 协议依赖 `@cptf/framework`(pnpm monorepo),改 `packages/framework` 后自动同步。 |
| 17 | - **包组织(@cptf npm 组织)**:所有包归 `@cptf` 域下,pnpm monorepo(双层 workspace)。`app` 经 `workspace:*` 依赖 `@cptf/framework` 等,改框架包后自动同步。详见下方「工程结构与构建打包」。 | 17 | - **包组织(@cptf npm 组织)**:所有包归 `@cptf` 域下,pnpm monorepo(双层 workspace)。`app` 经 `workspace:*` 依赖 `@cptf/framework` 等,改框架包后自动同步。详见下方「工程结构与构建打包」。 |
| 18 | +- **指标口径(单一真相)**:**所有测试报告(HTML / Markdown / Comparison JSON)的指标计算必须遵守 [METRICS-SPEC.md](packages/framework/docs/METRICS-SPEC.md)**。三态互斥用例模型(Unsupported / Passed / Failed)、通过率 = passedCases / executedCases、崩溃数独立统计、性能指标未启用时隐藏。任何新增指标展示必须走 `computeUnifiedMetrics()`。 | ||
| 19 | +- **用例字段语义(单一真相)**:**用例声明与运行时字段的语义必须遵守 [TEST-CASE-FIELDS-SPEC.md](packages/framework/docs/TEST-CASE-FIELDS-SPEC.md)**。`automationStatus`/`automationNote`/`skipped`/`success`/`knownIssue`/`verificationScope` 等字段的定义、传播规则、状态矩阵。 | ||
| 20 | +- **并发安全**:全局状态(`NavigationCoordinator` 等静态 Map)**不需要加锁**,依赖 Node.js 单线程原子性 + 设备级 Map 键隔离 + 默认串行执行。详见 [`CONCURRENCY-SAFETY.md`](packages/framework/docs/architecture/CONCURRENCY-SAFETY.md) 和 `cptf-arch` skill。 | ||
| 18 | - **编码规范**:禁止注释 / 禁止 emoji / 简洁输出 / 遵循现有文件风格。 | 21 | - **编码规范**:禁止注释 / 禁止 emoji / 简洁输出 / 遵循现有文件风格。 |
| 19 | - **禁止动作**:未经用户明确允许,不修改 git config / 不 force push / 不 commit / 不做 proactive actions。 | 22 | - **禁止动作**:未经用户明确允许,不修改 git config / 不 force push / 不 commit / 不做 proactive actions。 |
| 20 | 23 | ||
| 21 | -## 3层职责速查 | 24 | +## 五层职责速查(src/ 目录即五层) |
| 22 | 25 | ||
| 23 | | 层 | 职责 | | 26 | | 层 | 职责 | |
| 24 | |---|---| | 27 | |---|---| |
| 25 | -| 调度层 | ExecutionEngine / TestRunner / ComparisonRunner / UI自动化 / 指标采集 / 报告生成 / 导航管理 / @Test@Step | | 28 | +| 编排层 orchestration | TestRunner / ComparisonRunner / ExecutionEngine / ParallelTestRunner / 报告生成 / @Test@Step | |
| 26 | -| 系统层 | ISystem 7子接口 / HarmonySystem(UITest) / AndroidSystem(ATX) / IosSystem(WDA) / 统一降级策略 | | 29 | +| 能力层 capability | UIAutomation / ExpectExecutor / PerformanceCollector / VisualComparator / UIComparator | |
| 27 | -| 基础设施层 | 设备管理 / 端口管理(MetricPortPool) / MetricWS Server / 配置加载 / 用例扫描 / Logger / CacheManager / SmartWaiter / 类型定义 | | 30 | +| 系统层 system | ISystem 7子接口 / HarmonySystem(UITest+NativeDriver) / AndroidSystem(ATX+NativeDriver) / IosSystem(WDA) / SystemFactory | |
| 31 | +| 基础设施层 infrastructure | 配置加载 / 用例扫描(lazy-scanner) / 设备发现 / 索引生成 | | ||
| 32 | +| 基础层 base | 类型定义 / Logger / CacheManager / SmartWaiter / PortManager / JobQueue / MetricWS Server / Helpers | | ||
| 28 | 33 | ||
| 29 | ## CLI 速查 | 34 | ## CLI 速查 |
| 30 | 35 | ||
| @@ -45,6 +50,9 @@ cptf compare -t button-onpress # 按用例 | |||
| 45 | # 筛选 / 调试 | 50 | # 筛选 / 调试 |
| 46 | cptf run -m performance # 性能模块 | 51 | cptf run -m performance # 性能模块 |
| 47 | cptf run -t button-onpress # 指定用例 | 52 | cptf run -t button-onpress # 指定用例 |
| 53 | +cptf run --driver native # 灰盒模式(设备端原生执行,需测试 HAP) | ||
| 54 | +cptf run --driver external # 黑盒模式(外部 Agent,默认) | ||
| 55 | +cptf run --driver auto # 自动模式(优先 native,降级 external) | ||
| 48 | CPTF_LOG_LEVEL=debug cptf run # 调试日志 | 56 | CPTF_LOG_LEVEL=debug cptf run # 调试日志 |
| 49 | cptf scan # 扫描用例 | 57 | cptf scan # 扫描用例 |
| 50 | cptf index # 生成测试索引 | 58 | cptf index # 生成测试索引 |
| @@ -76,9 +84,13 @@ pnpm test -- -m <mod> # 单模块 | |||
| 76 | 84 | ||
| 77 | | 包(npm name) | 目录 | 形态 | 依赖 | | 85 | | 包(npm name) | 目录 | 形态 | 依赖 | |
| 78 | |---|---|---|---| | 86 | |---|---|---|---| |
| 79 | -| `@cptf/framework` | `packages/framework` | built dist(`main: dist/index.js`),bin `cptf` | 核心,无 workspace 依赖 | | 87 | +| `@cptf/framework` | `packages/framework` | built dist(`main: dist/index.js`),bin `cptf` | 核心,`@cptf/schema` | |
| 88 | +| `@cptf/schema` | `packages/schema` | built dist(跨宿主/设备共享类型) | 零依赖 | | ||
| 80 | | `@cptf/benchmark` | `packages/benchmark` | built dist(host 侧 collector) | `@cptf/framework` | | 89 | | `@cptf/benchmark` | `packages/benchmark` | built dist(host 侧 collector) | `@cptf/framework` | |
| 81 | | `@cptf/benchmark-rn` | `packages/benchmark-rn` | src(设备侧 JS + android kotlin + harmony cpp,metro 直接消费,无 dist) | peer `react-native` | | 90 | | `@cptf/benchmark-rn` | `packages/benchmark-rn` | src(设备侧 JS + android kotlin + harmony cpp,metro 直接消费,无 dist) | peer `react-native` | |
| 91 | +| `@cptf/native-harmony` | `packages/native-harmony` | harmony(设备侧 HAR,纯 ArkTS 实现 ohosTest Driver + NativeTestDriver.ets,无 C++) | `@cptf/framework`, `@cptf/schema` | | ||
| 92 | +| `@cptf/plugin-rn` | `packages/plugin-rn` | src(RN AppProbe 实现,预留能力,metro 直接消费,无 dist) | `@cptf/native-harmony`, peer `react-native` | | ||
| 93 | +| (非 npm 包)`native-android` | `packages/native-android` | android(DeviceDriver.java + WebSocketClient.java,Android native 驱动设备端,经 `app/android/app/build.gradle` java.srcDirs 直接消费,无 package.json) | — | | ||
| 82 | 94 | ||
| 83 | ```bash | 95 | ```bash |
| 84 | # 构建(在 cptf/ 根) | 96 | # 构建(在 cptf/ 根) |
| @@ -108,6 +120,10 @@ pnpm build:release # 全量:codegen + bundle + 三平台并行 native | |||
| 108 | ## 路由指引(用哪个 skill) | 120 | ## 路由指引(用哪个 skill) |
| 109 | 121 | ||
| 110 | - **改 packages/framework 代码** → `cptf-framework-dev` / `cptf-arch` / `cptf-system-extension` | 122 | - **改 packages/framework 代码** → `cptf-framework-dev` / `cptf-arch` / `cptf-system-extension` |
| 123 | +- **Harmony/Android native driver(HarmonyNativeDriver / AndroidNativeDriver / NativeTestDriver.ets / DeviceDriver.java / ohosTest / am instrument)** → `cptf-native-driver` | ||
| 124 | +- **资源管理规范(Timer / Worker / 子进程 / Stream / Socket / Map 清理)** → `cptf-resource-management` | ||
| 125 | +- **CPTF 测试规则(testID / @Step 格式 / 场景导航 / 设备稳定性)** → `cptf-rules` | ||
| 126 | +- **UITest 连接稳定性设计(TCP 重连 / 健康检查 / 布局提供者)** → `cptf-stability-design` | ||
| 111 | - **写测试用例 @Test/@Step** → `cptf-test-api` / `cptf-ui-locator` | 127 | - **写测试用例 @Test/@Step** → `cptf-test-api` / `cptf-ui-locator` |
| 112 | - **跑 CLI** → `cptf-cli` | 128 | - **跑 CLI** → `cptf-cli` |
| 113 | - **iOS 接入** → `cptf-ios` | 129 | - **iOS 接入** → `cptf-ios` |
| @@ -118,6 +134,8 @@ pnpm build:release # 全量:codegen + bundle + 三平台并行 native | |||
| 118 | - **RN 应用** → `cptf-rnoh-app`(根目录 `.opencode/skills/`) | 134 | - **RN 应用** → `cptf-rnoh-app`(根目录 `.opencode/skills/`) |
| 119 | - **调试 / 排错** → `cptf-troubleshoot` | 135 | - **调试 / 排错** → `cptf-troubleshoot` |
| 120 | - **自定义指标** → 从 `cptf-framework-dev` 进入(暂无独立 skill) | 136 | - **自定义指标** → 从 `cptf-framework-dev` 进入(暂无独立 skill) |
| 137 | +- **并发安全 / 状态管理** → `cptf-arch` skill 和 [CONCURRENCY-SAFETY.md](packages/framework/docs/architecture/CONCURRENCY-SAFETY.md) | ||
| 138 | +- **优化建议评估** → `cptf-optimization-verification`(静态分析误报识别、验证优先工作流、成本收益决策框架) | ||
| 121 | 139 | ||
| 122 | ## 变更日志 | 140 | ## 变更日志 |
| 123 | 141 | ||
| @@ -27,7 +27,7 @@ | |||
| 27 | 多设备场景下,每个设备的端口转发必须指向正确的主机端口。 | 27 | 多设备场景下,每个设备的端口转发必须指向正确的主机端口。 |
| 28 | 28 | ||
| 29 | - **rport 不支持覆盖**:`hdc rport` 在端口已占用时直接失败。设置新 rport 前必须先 `fport rm` 清理旧的。 | 29 | - **rport 不支持覆盖**:`hdc rport` 在端口已占用时直接失败。设置新 rport 前必须先 `fport rm` 清理旧的。 |
| 30 | -- **使用 actualPort**:当首选端口 18081 被占用,`MetricPortPool` 分配 18091/18092 等。rport/reverse 的 host 侧必须用 `actualPort`,不能用硬编码的 18081。 | 30 | +- **使用 actualPort**:当首选端口 18081 被占用,`PortManager`(`base/utils/port-pool.ts`)分配 18091/18092 等。rport/reverse 的 host 侧必须用 `actualPort`,不能用硬编码的 18081。 |
| 31 | - **测试后清理**:`stopMetricWsServer` 必须移除 rport,避免遗留影响下次测试。 | 31 | - **测试后清理**:`stopMetricWsServer` 必须移除 rport,避免遗留影响下次测试。 |
| 32 | 32 | ||
| 33 | ### 1.4 时钟 | 33 | ### 1.4 时钟 |
| @@ -47,7 +47,7 @@ | |||
| 47 | 47 | ||
| 48 | ### 2.2 崩溃恢复 | 48 | ### 2.2 崩溃恢复 |
| 49 | 49 | ||
| 50 | -- **恢复上限**:`MAX_CRASH_RECOVERIES = 2`。达到上限后跳过用例,不无限重试。 | 50 | +- **恢复上限**:`MAX_CRASH_RECOVERIES = 5`(`test-scene-navigator.ts:14`)。达到上限后跳过用例,不无限重试。 |
| 51 | - **状态重置**:恢复后必须重置导航状态(当前页面、deep link),防止在错误页面上继续执行。 | 51 | - **状态重置**:恢复后必须重置导航状态(当前页面、deep link),防止在错误页面上继续执行。 |
| 52 | - **降级而非阻塞**:UITest 连接断开时自动重连,重连失败则降级到 hdc shell,不阻塞测试流程。 | 52 | - **降级而非阻塞**:UITest 连接断开时自动重连,重连失败则降级到 hdc shell,不阻塞测试流程。 |
| 53 | 53 | ||
| @@ -94,7 +94,7 @@ WebSocket 事件(主通道) | |||
| 94 | 94 | ||
| 95 | ### 3.4 端口池管理 | 95 | ### 3.4 端口池管理 |
| 96 | 96 | ||
| 97 | -- **按设备分配**:`MetricPortPool` 为每个设备分配独立端口,首选 18081,冲突时递增到 18091、18092。 | 97 | +- **按设备分配**:`PortManager` 为每个设备分配独立端口,首选 18081,冲突时递增到 18091、18092。 |
| 98 | - **用后释放**:`releaseMetricServer` 释放端口,引用计数归零时停止服务器。 | 98 | - **用后释放**:`releaseMetricServer` 释放端口,引用计数归零时停止服务器。 |
| 99 | - **不累积**:同设备的多个用例复用同一服务器(`refCount` 递增),不为每个用例创建新服务器。 | 99 | - **不累积**:同设备的多个用例复用同一服务器(`refCount` 递增),不为每个用例创建新服务器。 |
| 100 | 100 | ||
| @@ -9,7 +9,7 @@ | |||
| 9 | | 特性 | CPTF | 其他框架 | | 9 | | 特性 | CPTF | 其他框架 | |
| 10 | |------|------|---------| | 10 | |------|------|---------| |
| 11 | | **HarmonyOS 支持** | ✅ 原生支持 | ❌ 不支持 | | 11 | | **HarmonyOS 支持** | ✅ 原生支持 | ❌ 不支持 | |
| 12 | -| **性能指标采集** | ✅ 13种性能指标+3种稳定性指标(真实FPS) | ⚠️ 2-3种(模拟FPS) | | 12 | +| **性能指标采集** | ✅ 19种性能指标+3种稳定性指标(真实FPS) | ⚠️ 2-3种(模拟FPS) | |
| 13 | | **稳定性测试** | ✅ SmartMonkey + ANR + 白屏检测 + 内存泄漏 + 崩溃恢复 | ❌ 需自行搭建 | | 13 | | **稳定性测试** | ✅ SmartMonkey + ANR + 白屏检测 + 内存泄漏 + 崩溃恢复 | ❌ 需自行搭建 | |
| 14 | | **跨平台对比** | ✅ HarmonyOS vs Android | ⚠️ 需自行搭建 | | 14 | | **跨平台对比** | ✅ HarmonyOS vs Android | ⚠️ 需自行搭建 | |
| 15 | | **用例编写** | ✅ 声明式(减少70-90%代码) | ⚠️ 命令式 | | 15 | | **用例编写** | ✅ 声明式(减少70-90%代码) | ⚠️ 命令式 | |
| @@ -17,16 +17,16 @@ | |||
| 17 | **核心亮点**: | 17 | **核心亮点**: |
| 18 | - **唯一性**:业界唯一支持 HarmonyOS 自动化测试 | 18 | - **唯一性**:业界唯一支持 HarmonyOS 自动化测试 |
| 19 | - **精准性**:真实 FPS 采集(RenderService fpsCount + hitchs),非模拟数据 | 19 | - **精准性**:真实 FPS 采集(RenderService fpsCount + hitchs),非模拟数据 |
| 20 | -- **完整性**:13种性能指标 + 3种稳定性指标 + 完整稳定性测试套件(SmartMonkey/白屏/内存泄漏/ANR/崩溃恢复) | 20 | +- **完整性**:19种性能指标 + 3种稳定性指标 + 完整稳定性测试套件(SmartMonkey/白屏/内存泄漏/ANR/崩溃恢复) |
| 21 | - **简洁性**:声明式框架 `@Test/@Step`,代码量减少 70-90% | 21 | - **简洁性**:声明式框架 `@Test/@Step`,代码量减少 70-90% |
| 22 | -- **专业性**:3层架构设计,职责分离清晰,易于扩展 | 22 | +- **专业性**:五层架构设计(src/ 目录即五层),职责分离清晰,易于扩展 |
| 23 | 23 | ||
| 24 | ## 业界最佳实践 | 24 | ## 业界最佳实践 |
| 25 | 25 | ||
| 26 | **React Native for OpenHarmony (RNOH)** 基于 CPTF 框架完成全量功能、性能、稳定性测试,是目前业界最大的 HarmonyOS 跨端测试实践: | 26 | **React Native for OpenHarmony (RNOH)** 基于 CPTF 框架完成全量功能、性能、稳定性测试,是目前业界最大的 HarmonyOS 跨端测试实践: |
| 27 | 27 | ||
| 28 | - **功能测试**:覆盖 Switch、Button、RefreshControl 等核心组件的全量属性跨平台一致性验证 | 28 | - **功能测试**:覆盖 Switch、Button、RefreshControl 等核心组件的全量属性跨平台一致性验证 |
| 29 | -- **性能测试**:13 种指标采集 + Warmup迭代 + 阈值告警 + HTML趋势图 | 29 | +- **性能测试**:19 种指标采集 + Warmup迭代 + 阈值告警 + HTML趋势图 |
| 30 | - **稳定性测试**:SmartMonkey + ANR检测 + 白屏检测 + 内存泄漏 + 崩溃自动恢复 | 30 | - **稳定性测试**:SmartMonkey + ANR检测 + 白屏检测 + 内存泄漏 + 崩溃自动恢复 |
| 31 | - **跨平台对比**:HarmonyOS vs Android UI结构4维对比(属性/样式/位置/层级) | 31 | - **跨平台对比**:HarmonyOS vs Android UI结构4维对比(属性/样式/位置/层级) |
| 32 | 32 | ||
| @@ -45,7 +45,7 @@ | |||
| 45 | ``` | 45 | ``` |
| 46 | cptf/ | 46 | cptf/ |
| 47 | ├── packages/framework/ # 框架层(npm 包 @cptf/framework) | 47 | ├── packages/framework/ # 框架层(npm 包 @cptf/framework) |
| 48 | -│ └── src/ # 3层架构源码 | 48 | +│ └── src/ # 五层架构源码(base/infrastructure/system/capability/orchestration) |
| 49 | │ ├── base/ # 基础设施层(类型/配置/工具/设备管理/端口管理) | 49 | │ ├── base/ # 基础设施层(类型/配置/工具/设备管理/端口管理) |
| 50 | │ ├── infrastructure/ # 基础设施层(扫描/索引/设备发现) | 50 | │ ├── infrastructure/ # 基础设施层(扫描/索引/设备发现) |
| 51 | │ ├── system/ # 系统层(ISystem接口+平台实现+Agent客户端) | 51 | │ ├── system/ # 系统层(ISystem接口+平台实现+Agent客户端) |
| @@ -130,7 +130,7 @@ cptf run -p harmony | |||
| 130 | - CPU 使用率 | 130 | - CPU 使用率 |
| 131 | - 内存占用(含泄漏检测:线性回归+R²置信度) | 131 | - 内存占用(含泄漏检测:线性回归+R²置信度) |
| 132 | - 白屏检测(PNG+zlib解析,亮度>240+白像素比>85%) | 132 | - 白屏检测(PNG+zlib解析,亮度>240+白像素比>85%) |
| 133 | -- 13种性能指标 + 3种稳定性指标 + HTML趋势图 | 133 | +- 19种性能指标 + 3种稳定性指标 + HTML趋势图 |
| 134 | 134 | ||
| 135 | ### 稳定性测试 | 135 | ### 稳定性测试 |
| 136 | 136 | ||
| @@ -174,7 +174,7 @@ cptf template <module> -n <name> # 生成测试用例模板 | |||
| 174 | | 文档 | 说明 | | 174 | | 文档 | 说明 | |
| 175 | |------|------| | 175 | |------|------| |
| 176 | | [架构总览](packages/framework/docs/architecture/ARCHITECTURE.md) | 框架架构、源码目录、依赖规则 | | 176 | | [架构总览](packages/framework/docs/architecture/ARCHITECTURE.md) | 框架架构、源码目录、依赖规则 | |
| 177 | -| [架构优势](packages/framework/docs/architecture/ARCHITECTURE-OVERVIEW.md) | 3层架构、三平台加速、性能优势 | | 177 | +| [架构优势](packages/framework/docs/architecture/ARCHITECTURE-OVERVIEW.md) | 五层架构、三平台加速、性能优势 | |
| 178 | | [能力规格](packages/framework/docs/architecture/CAPABILITY-SPEC.md) | 性能指标、API 规格、数据结构 | | 178 | | [能力规格](packages/framework/docs/architecture/CAPABILITY-SPEC.md) | 性能指标、API 规格、数据结构 | |
| 179 | | [用户指南](packages/framework/docs/getting-started/USER-GUIDE.md) | 安装、配置、CLI、API 使用 | | 179 | | [用户指南](packages/framework/docs/getting-started/USER-GUIDE.md) | 安装、配置、CLI、API 使用 | |
| 180 | | [声明式测试指南](packages/framework/docs/getting-started/DECLARATIVE-TEST-GUIDE.md) | @Test/@Step 装饰器、TestConfig 详解 | | 180 | | [声明式测试指南](packages/framework/docs/getting-started/DECLARATIVE-TEST-GUIDE.md) | @Test/@Step 装饰器、TestConfig 详解 | |
Rpackages/framework/src/system/harmony/port-manager.ts→packages/framework/src/base/utils/port-pool.ts+134-14
Rpackages/framework/src/capability/expect/index.ts→packages/native-harmony/harmony/nativeharmony/Index.ets+2-1