diff --git a/.gitignore b/.gitignore index 40a870a..630c5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ Desktop.ini /测试相关资料/如何测试/本机测试环境/.venv/ /测试相关资料/如何测试/本机测试环境/data/ /测试相关资料/如何测试/本机测试环境/work/ +/测试相关资料/如何测试/本机测试环境/.local/ /.playwright-cli/ /output/playwright/ diff --git a/AGENTS.md b/AGENTS.md index 36fe5e2..7445812 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,8 @@ ## 9. 测试经验与交付反馈 +项目专用本机测试环境统一放在 `测试相关资料/如何测试/本机测试环境/.local/` 并精确忽略;不要写入全局 Codex 工具目录。可复建入口、锁定依赖、校验信息和搭建说明必须提交并随相关测试同步更新。通用工具仍按全局登记复用。复制工作区后重建 Python venv、更新本机工具配置和 AVD 路径;不得把带旧绝对路径的环境当作可移植工具。Android 必需的临时 ASCII 目录联接只用于入口,材料实际保存在项目内,清理只删除链接本身。具体命令见本机测试环境 README。 + 每轮测试完成后,将测试中遇到的问题及已验证解法补到对应可重复测试流程、排障指南或构建文档,防止后续重复试错;单次失败证据、恢复及重测结果留在测试归档。已知问题先查已有经验,不把未证实推测固化成根因。最终反馈说明经验更新位置和仍未解决的边界;没有新增经验时沿用已有记录,不重复复制。文档不得包含真实凭据、测试设备标识或开发机绝对路径。 ## Android 按影响选测(当前规则) diff --git a/发布更新相关/镜像编辑器/编辑器源代码/README.md b/发布更新相关/镜像编辑器/编辑器源代码/README.md index b1999a0..c7c93d8 100644 --- a/发布更新相关/镜像编辑器/编辑器源代码/README.md +++ b/发布更新相关/镜像编辑器/编辑器源代码/README.md @@ -13,6 +13,8 @@ 脚本创建隔离构建环境、运行单元测试,再使用 PyInstaller 生成单 EXE,并更新 `../编辑器程序/` 的版本和 SHA-256。发布后的电脑不需要安装 Python、.NET 或 Qt。 已有锁定依赖的 venv 时可传入 `-BuildEnvironment ""`,脚本会拒绝错误的 PySide6/PyInstaller 版本。 +本项目已把专用构建环境从全局工具目录迁入仓库内被忽略的本机测试目录。先按 [本机环境说明](../../../测试相关资料/如何测试/本机测试环境/README.md) 在仓库根运行 `local_env.py setup image-editor`,再从本目录调用 `build_windows.ps1 -BuildEnvironment "../../../测试相关资料/如何测试/本机测试环境/.local/image-editor/venv"`。环境及 wheel 不提交,复建入口、依赖版本与校验值跟踪维护;更换电脑或工作区位置须重建 venv。本次整理不重新发布编辑器 EXE。 + ## macOS 必须在 macOS 上本地构建,PyInstaller 不是跨平台编译器: diff --git a/各种归档/2026-10-01_本机环境迁移/README.md b/各种归档/2026-10-01_本机环境迁移/README.md new file mode 100644 index 0000000..0cc098f --- /dev/null +++ b/各种归档/2026-10-01_本机环境迁移/README.md @@ -0,0 +1,38 @@ +# 2026-10-01 项目本机测试环境迁移 + +## 结果与范围 + +- 项目 SSH、镜像编辑器专用 Python 环境在本机测试目录 `.local/` 重建,按原环境版本锁定完整依赖与 Windows wheel SHA-256;全部离线安装并通过版本校验和 pip check。 +- API 35 / Google APIs / x86_64 revision 9 系统镜像迁入项目;保留官方包登记和许可证,新增项目专用 `qms-local-api35` AVD。未重新下载系统镜像,未声称重新验证已不存在的历史 ZIP。 +- 三个旧项目专用全局目录已移出 `.codex-tools/bin`,TOOLS.md 对应三项登记已删除;通用 APK/ADB/Java/Gradle/SSH/Emulator 等工具保留。旧 SDK Emulator 目录联接仅移除链接本身,共享 Emulator 可执行文件仍存在。 +- 旧项目 AVD 索引指向的实际数据目录,以及旧 SSH 主机密钥文件均不存在。失效索引保存在迁移备份;缺失数据没有伪造为成功迁移,没有连接设备建立新的信任。 +- 自动审批拒绝批量及单目录递归删除,返回 `blocked by policy`,未提供进一步原因。采用无损迁回项目的方案:旧 venv/SDK 元数据和工具登记快照保存在被忽略的 `.local/private/migration/`,测试入口只使用新环境。用户可删除该备份目录。 + +## 选择验证与证据 + +本轮影响是主机环境归属、测试私有路径及 Android 路径入口。选择依赖/解释器、路径保护、测试场景选择、编辑器单元测试,以及独立模拟器 settings/baseline;没有产品行为变化,不跑无关页面、BLE、真实切网、性能或 systemd 生命周期验证。 + +| 验证 | 实际结果 | +| --- | --- | +| SSH、编辑器依赖版本与 pip check | 通过;SSH 模块导入及项目内私有路径核对通过,未发起 SSH | +| 迁移/路径/Windows 联接生命周期及依赖更新门禁单元测试 | 7 项通过 | +| Android 场景选择及不访问设备保护测试 | 9 项通过 | +| 镜像编辑器单元测试 | 14 项通过;未重新发布 EXE | +| 主工作区独立 uiTest debug 构建 | 通过;只构建测试 APK,未发布产品 APK | +| 主工作区 settings/baseline 模拟器 | 通过;字体、方向、密度恢复无错误,只关闭本次进程 | +| 中文及空格的隔离副本 | 全部 12 个验证步骤通过,见下文 | +| Git 边界 | 新环境、镜像、私有状态、日志被忽略;搭建入口、锁文件、说明和测试源码未被忽略 | +| 仓库卫生与 diff 空白检查 | 通过;没有输出秘密值 | + +隔离副本复制 662 个 Git 候选文件的实际内容,不复制 venv、真实凭据、手机登记或主机密钥。仅复用版本锁定的 wheel 与迁入项目的系统镜像文件;在新位置重新创建两个 venv、生成本机工具配置和全新项目 AVD。12 步包括三组 setup/check、迁移保护、9 项场景选择、14 项编辑器测试、按文档 `build-ui android`、settings/baseline 冒烟和完成后的 Android 路径检查,全部通过。最后补充依赖更新门禁后,同步隔离入口并复核 7 项主机测试及两组 Python check,均通过。通用已安装工具是文档明确的前置条件,没有调用旧项目专用全局环境。 + +本机原始日志、JSON、构建和隔离副本位于 `.local/work/`,包含本机路径,不提交。隔离副本 `.local/work/隔离 复建/` 在查验后可删除。仓库只保留这份脱敏结果。 + +## 问题、修正与复测 + +1. 中文路径下 Emulator 能发现 AVD,却在开机前退出;日志显示写文件失败、中文路径损坏及 kernel 无法读取。用 ASCII 临时目录联接后同一镜像的 settings/baseline 通过。复建入口现在自动提供临时索引和路径,实际文件仍在项目内,并在结束后只删除联接及临时索引。 +2. Android 构建入口原先 resolve 了 ASCII 联接,展开为中文目标后被自己的路径门禁拒绝;保留显式 ASCII 拼写后主工作区和隔离副本均构建通过。 +3. Emulator 把 config.ini 重写为带键值空格的格式,严格逐行匹配产生失效路径误报;改为按 INI 键值修剪空白,并在会话结束恢复持久 image 路径。完成后的路径检查和联接生命周期测试均通过。 +4. 读取启动日志时 Python GBK 输出替换字符触发编码错误;本次诊断进程显式使用 `-X utf8`,不改系统编码。 + +复用经验已写入本机测试环境 README、Android 常见问题与排障、构建说明及协作规则。未测试的真实 SSH/真机/板端/实屏不计入本轮通过结论;产品接口、协议、驱动、发布版本和 FEATURE_UPDATED_AT 未修改。 diff --git a/整体开发需求/01_网页配置端需求.md b/整体开发需求/01_网页配置端需求.md index 03134dc..9de9869 100644 --- a/整体开发需求/01_网页配置端需求.md +++ b/整体开发需求/01_网页配置端需求.md @@ -318,6 +318,7 @@ WantedBy=multi-user.target - 测试入口位于 `测试相关资料/如何测试/本机测试环境/`,发布工具从 `发布更新相关/` 定位离线材料和产物;源码仍位于根目录 `核桃派软件源代码/`。路径须由脚本位置或显式参数解析,支持中文、空格及不同调用目录。 - 工作区搬迁不改变设备安装路径、镜像内部载荷目录或历史包协议;已有发布记录仅同步工作区产物路径,不改版本、摘要及历史验收结果。 - 本机测试与清理必须在隔离工作区验证:能够定位本地源码,保留 `.venv` 和人工持久数据,并拒绝清理越界路径。覆盖见 `TEST-WORKSPACE-LAYOUT`。 +- 项目专用 SSH/编辑器 venv、测试 SDK/AVD、临时脚本与私有状态统一保存在本机测试环境 `.local/` 并精确忽略;可复建入口、完整依赖版本/哈希、官方镜像包登记和搭建经验跟踪维护。通用工具可复用,项目专用材料不得污染全局工具目录。复制后重建 venv、刷新 AVD 路径;Android 必需的临时 ASCII 联接只提供入口,数据仍在项目内,清理不得遍历链接目标。覆盖见 `TEST-LOCAL-ENV`。 ### Git 仓库安全约定(`DEPLOY-REPOSITORY-HYGIENE`) diff --git a/测试相关资料/如何测试/本机测试环境/.gitignore b/测试相关资料/如何测试/本机测试环境/.gitignore index b951a0d..a2f1b8e 100644 --- a/测试相关资料/如何测试/本机测试环境/.gitignore +++ b/测试相关资料/如何测试/本机测试环境/.gitignore @@ -1,4 +1,5 @@ .venv/ +.local/ data/ work/ __pycache__/ diff --git a/测试相关资料/如何测试/本机测试环境/README.md b/测试相关资料/如何测试/本机测试环境/README.md index c00f14a..1e6ee70 100644 --- a/测试相关资料/如何测试/本机测试环境/README.md +++ b/测试相关资料/如何测试/本机测试环境/README.md @@ -43,3 +43,74 @@ python ".\测试相关资料\如何测试\本机测试环境\local_test.py" clea `clean` 只删除可再生产物:`work/`、整个 `data/runtime/`、源码缓存、覆盖率文件和已知浏览器测试产物。它不会清空 `data/`、扫描 `%TEMP%` 或整盘,也不会自动删除损坏的 `.venv`。测试产生的配置/模板反例必须留在隔离临时目录并由测试框架回收,不能把损坏样例或多代备份积累在本目录。 完整的分层测试选择、远端冒烟和实屏档位见上一级的《核桃派点阵屏控制服务测试流程.md》。 + +## 项目专用本机环境(2026-10-01) + +项目专用环境统一保存在本目录 `.local/`,整个目录被 Git 精确忽略。通用 ADB、APK 检查器、Java、Gradle、Android 命令行工具和 Emulator 仍可复用本机正常安装或全局登记的工具;不要把项目测试脚本、专用 venv、AVD 或私有状态放回全局工具目录。 + +| 目录 | 用途 | 保留规则 | +| --- | --- | --- | +| `.local/ssh/` | SSH 联调 venv、锁定 wheel 与校验清单 | 可重建;不复制凭据 | +| `.local/image-editor/` | 编辑器构建 venv、锁定 wheel 与校验清单 | 可重建;发布 EXE 另按原流程保存 | +| `.local/android/` | 测试 SDK 系统镜像、专用 AVD、模拟器状态 | 包可复建;保留 AVD 用户数据 | +| `.local/private/` | SSH 主机密钥、本机私有状态及短期 WiFi 测试材料 | 不提交;不自动清空或覆盖 | +| `.local/work/` | 临时脚本、下载、隔离构建、日志和报告 | 验证后仅按本次精确路径清理 | +| `.local/tools.local.json` | 本机通用工具的实际位置 | 不提交;新机器重新生成或显式指定 | + +### 搭建与检查 + +命令从仓库根执行。锁定环境要求 **Windows x64 / 官方 CPython 3.14 x64**;其他平台沿用各自构建说明。脚本由自身位置定位仓库,不依赖当前目录。下面的 Python `-X utf8` 只用于本次进程的中文诊断输出,不修改系统编码或环境设置。 + +```powershell +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" setup ssh +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" setup image-editor +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" setup android +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" check ssh +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" check image-editor +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" check android +``` + +SSH 与编辑器依赖使用 `requirements-*.lock.txt` 锁定完整依赖闭包和 Windows wheel SHA-256;来源是官方 PyPI。先复用 pip 缓存或已有 wheel,下载到各自 `.local/<环境>/wheels/`,再以 `--no-index --find-links --require-hashes` 安装。已有 wheel 可加 `--offline`;缺少文件或哈希不符就失败,不回退到在线安装。安装后检查依赖版本、`pip check` 和 venv 创建位置。 + +setup/check 同时核对原始 `requirements-host.txt` 或编辑器 `requirements-build.txt` 与完整锁文件;原始依赖更新但锁文件未同步时直接失败,提示更新依赖闭包与哈希,防止继续使用过期测试环境。 + +Android 镜像固定为 API 35 / Google APIs / x86_64 revision 9,Google 官方 URL 与发布 SHA-1 在 `android-image.json`;官方包登记与许可证在 `android-package.xml`。首次下载先核对官方 SHA-1,再记录本机 SHA-256,保留镜像内版本信息。可用 `setup android --image-archive "<官方ZIP路径>" --offline` 复用既有下载;也可把 SDK Manager 已安装并含 `package.xml` 的同版本目录复制到 `.local/android/sdk/system-images/android-35/google_apis/x86_64/` 后执行 setup。已有镜像不重复下载。 + +通用工具的本机位置保存在 `.local/tools.local.json`。自动定位只检查少量已知位置;未找到时显式提供参数,不扫描硬盘、不下载或安装 Android Studio: + +```powershell +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" setup android --offline --adb "" --emulator "" --jdk "" --sdk "<正常安装的Android SDK目录>" +``` + +SSH 联调用 `.local/ssh/venv/Scripts/python.exe` 运行原有测试脚本;仍必须遵守凭据和手机登记门禁。SSH 主机密钥固定保存在 `.local/private/qms-mobile-known_hosts`。首次信任与变更密钥拒绝策略保持原实现;没有旧文件时只能说明“未保留旧信任”,不能声称已经迁移。WiFi 临时秘密写在同一私有目录并沿用原测试的 finally 清理。 + +### 模拟器与中文路径 + +独立 AVD 名称为 `qms-local-api35`,不登记到用户全局 AVD 目录。setup 只刷新本工具创建的 AVD 路径,不 wipe-data;遇到未受本工具管理的同名目录会拒绝覆盖。 + +Android Emulator 的底层文件访问在本机中文路径下启动失败。`build-ui` 和 `ui-smoke` 会在进程临时目录创建一次性的 ASCII 目录联接,指向项目 `.local/`;实际镜像、AVD、构建与报告仍保存在项目内。结束后只删除联接和临时索引,不遍历联接目标。模拟器不开窗口、不开摄像头,仅运行选中的 settings/baseline 场景;只关闭本次启动的模拟器。 + +```powershell +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" build-ui android +python -X utf8 "测试相关资料/如何测试/本机测试环境/local_env.py" ui-smoke android --build "测试相关资料/如何测试/本机测试环境/.local/work/android-build/<本次qms-build目录>" --report "测试相关资料/如何测试/本机测试环境/.local/work/<新的报告文件>.json" +``` + +`build-ui` 仅构建独立 uiTest debug APK,不发布产品 APK。报告已经存在时拒绝覆盖。完整场景仍按移动端“按影响选测”,不因环境检查扩展到真机、BLE、切网或全量 UI。若系统 TEMP 含中文,需要仅为当前进程选择可写的 ASCII 临时目录;不改系统设置。 + +### 复制项目与持续维护 + +Git 克隆后按上述命令重新搭建。复制整个工作区时,旧 venv 含绝对位置和旧 Python 绑定,不能当作可移植程序:先保留并改名旧 `.local/ssh/venv`、`.local/image-editor/venv`,再 setup;wheel 和数据可继续复用。Android 在新机器重新配置工具路径并 setup,刷新本工具管理的 AVD 路径。源码入口、锁文件、搭建文档与包登记必须随测试修订一起维护。 + +每轮相关测试先查已有排障记录;已验证解法更新本 README 或移动端排障,依赖更新同时维护原始 requirements、完整锁文件和 wheel 哈希。临时修补不能长期只存在 `.local/work/`:通用化且脱敏后加入可复用源码,或把复建步骤写进文档。单次日志保留在 `.local/work/`;需要归档的结果先脱敏再写入 `各种归档/`。 + +本次迁移的旧环境和失效索引保存在 `.local/private/migration/`,只作备份,不被任何测试入口使用;新环境验证后可由用户删除整个备份目录。一次性隔离副本 `.local/work/隔离 复建/` 可在查验本次结果后删除。不要因此删除 `.local/private/` 中其他私有状态,也不要清空仍在使用的 SSH/编辑器新环境或 Android 镜像。 + +迁移/复建验证命令: + +```powershell +python -X utf8 -m unittest discover -s "测试相关资料/如何测试/本机测试环境" -p "test_local_env.py" -v +python -X utf8 -m unittest discover -s "移动端相关内容/安卓app/如何安卓测试" -p "test_test_selection.py" -v +python -X utf8 "核桃派软件源代码/scripts/check_repository_hygiene.py" +``` + +独立性验收须把跟踪源码和必要测试入口复制到另一处带中文及空格的隔离目录,**不复制 venv 和私有登记**,从锁定依赖重建后验证。环境准备成功不等同于 SSH、真机、板端或实屏验收通过。 diff --git a/测试相关资料/如何测试/本机测试环境/android-image.json b/测试相关资料/如何测试/本机测试环境/android-image.json new file mode 100644 index 0000000..483e0dd --- /dev/null +++ b/测试相关资料/如何测试/本机测试环境/android-image.json @@ -0,0 +1,8 @@ +{ + "package": "system-images;android-35;google_apis;x86_64", + "revision": 9, + "url": "https://dl.google.com/android/repository/sys-img/google_apis/x86_64-35_r09.zip", + "sha1": "0103e6dab21290c4b9d16550a3ce99476f884eef", + "avd": "qms-local-api35", + "source": "Google Android SDK Repository; archive checksum recorded from official package metadata" +} diff --git a/测试相关资料/如何测试/本机测试环境/android-package.xml b/测试相关资料/如何测试/本机测试环境/android-package.xml new file mode 100644 index 0000000..68500a0 --- /dev/null +++ b/测试相关资料/如何测试/本机测试环境/android-package.xml @@ -0,0 +1,169 @@ + +Terms and Conditions + +This is the Android Software Development Kit License Agreement + +1. Introduction + +1.1 The Android Software Development Kit (referred to in the License Agreement as the "SDK" and specifically including the Android system files, packaged APIs, and Google APIs add-ons) is licensed to you subject to the terms of the License Agreement. The License Agreement forms a legally binding contract between you and Google in relation to your use of the SDK. + +1.2 "Android" means the Android software stack for devices, as made available under the Android Open Source Project, which is located at the following URL: http://source.android.com/, as updated from time to time. + +1.3 A "compatible implementation" means any Android device that (i) complies with the Android Compatibility Definition document, which can be found at the Android compatibility website (http://source.android.com/compatibility) and which may be updated from time to time; and (ii) successfully passes the Android Compatibility Test Suite (CTS). + +1.4 "Google" means Google Inc., a Delaware corporation with principal place of business at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States. + + +2. Accepting the License Agreement + +2.1 In order to use the SDK, you must first agree to the License Agreement. You may not use the SDK if you do not accept the License Agreement. + +2.2 By clicking to accept, you hereby agree to the terms of the License Agreement. + +2.3 You may not use the SDK and may not accept the License Agreement if you are a person barred from receiving the SDK under the laws of the United States or other countries, including the country in which you are resident or from which you use the SDK. + +2.4 If you are agreeing to be bound by the License Agreement on behalf of your employer or other entity, you represent and warrant that you have full legal authority to bind your employer or such entity to the License Agreement. If you do not have the requisite authority, you may not accept the License Agreement or use the SDK on behalf of your employer or other entity. + + +3. SDK License from Google + +3.1 Subject to the terms of the License Agreement, Google grants you a limited, worldwide, royalty-free, non-assignable, non-exclusive, and non-sublicensable license to use the SDK solely to develop applications for compatible implementations of Android. + +3.2 You may not use this SDK to develop applications for other platforms (including non-compatible implementations of Android) or to develop another SDK. You are of course free to develop applications for other platforms, including non-compatible implementations of Android, provided that this SDK is not used for that purpose. + +3.3 You agree that Google or third parties own all legal right, title and interest in and to the SDK, including any Intellectual Property Rights that subsist in the SDK. "Intellectual Property Rights" means any and all rights under patent law, copyright law, trade secret law, trademark law, and any and all other proprietary rights. Google reserves all rights not expressly granted to you. + +3.4 You may not use the SDK for any purpose not expressly permitted by the License Agreement. Except to the extent required by applicable third party licenses, you may not copy (except for backup purposes), modify, adapt, redistribute, decompile, reverse engineer, disassemble, or create derivative works of the SDK or any part of the SDK. + +3.5 Use, reproduction and distribution of components of the SDK licensed under an open source software license are governed solely by the terms of that open source software license and not the License Agreement. + +3.6 You agree that the form and nature of the SDK that Google provides may change without prior notice to you and that future versions of the SDK may be incompatible with applications developed on previous versions of the SDK. You agree that Google may stop (permanently or temporarily) providing the SDK (or any features within the SDK) to you or to users generally at Google's sole discretion, without prior notice to you. + +3.7 Nothing in the License Agreement gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, or other distinctive brand features. + +3.8 You agree that you will not remove, obscure, or alter any proprietary rights notices (including copyright and trademark notices) that may be affixed to or contained within the SDK. + + +4. Use of the SDK by You + +4.1 Google agrees that it obtains no right, title or interest from you (or your licensors) under the License Agreement in or to any software applications that you develop using the SDK, including any intellectual property rights that subsist in those applications. + +4.2 You agree to use the SDK and write applications only for purposes that are permitted by (a) the License Agreement and (b) any applicable law, regulation or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries). + +4.3 You agree that if you use the SDK to develop applications for general public users, you will protect the privacy and legal rights of those users. If the users provide you with user names, passwords, or other login information or personal information, you must make the users aware that the information will be available to your application, and you must provide legally adequate privacy notice and protection for those users. If your application stores personal or sensitive information provided by users, it must do so securely. If the user provides your application with Google Account information, your application may only use that information to access the user's Google Account when, and for the limited purposes for which, the user has given you permission to do so. + +4.4 You agree that you will not engage in any activity with the SDK, including the development or distribution of an application, that interferes with, disrupts, damages, or accesses in an unauthorized manner the servers, networks, or other properties or services of any third party including, but not limited to, Google or any mobile communications carrier. + +4.5 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any data, content, or resources that you create, transmit or display through Android and/or applications for Android, and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so. + +4.6 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any breach of your obligations under the License Agreement, any applicable third party contract or Terms of Service, or any applicable law or regulation, and for the consequences (including any loss or damage which Google or any third party may suffer) of any such breach. + +5. Your Developer Credentials + +5.1 You agree that you are responsible for maintaining the confidentiality of any developer credentials that may be issued to you by Google or which you may choose yourself and that you will be solely responsible for all applications that are developed under your developer credentials. + +6. Privacy and Information + +6.1 In order to continually innovate and improve the SDK, Google may collect certain usage statistics from the software including but not limited to a unique identifier, associated IP address, version number of the software, and information on which tools and/or services in the SDK are being used and how they are being used. Before any of this information is collected, the SDK will notify you and seek your consent. If you withhold consent, the information will not be collected. + +6.2 The data collected is examined in the aggregate to improve the SDK and is maintained in accordance with Google's Privacy Policy. + + +7. Third Party Applications + +7.1 If you use the SDK to run applications developed by a third party or that access data, content or resources provided by a third party, you agree that Google is not responsible for those applications, data, content, or resources. You understand that all data, content or resources which you may access through such third party applications are the sole responsibility of the person from which they originated and that Google is not liable for any loss or damage that you may experience as a result of the use or access of any of those third party applications, data, content, or resources. + +7.2 You should be aware the data, content, and resources presented to you through such a third party application may be protected by intellectual property rights which are owned by the providers (or by other persons or companies on their behalf). You may not modify, rent, lease, loan, sell, distribute or create derivative works based on these data, content, or resources (either in whole or in part) unless you have been specifically given permission to do so by the relevant owners. + +7.3 You acknowledge that your use of such third party applications, data, content, or resources may be subject to separate terms between you and the relevant third party. In that case, the License Agreement does not affect your legal relationship with these third parties. + + +8. Using Android APIs + +8.1 Google Data APIs + +8.1.1 If you use any API to retrieve data from Google, you acknowledge that the data may be protected by intellectual property rights which are owned by Google or those parties that provide the data (or by other persons or companies on their behalf). Your use of any such API may be subject to additional Terms of Service. You may not modify, rent, lease, loan, sell, distribute or create derivative works based on this data (either in whole or in part) unless allowed by the relevant Terms of Service. + +8.1.2 If you use any API to retrieve a user's data from Google, you acknowledge and agree that you shall retrieve data only with the user's explicit consent and only when, and for the limited purposes for which, the user has given you permission to do so. If you use the Android Recognition Service API, documented at the following URL: https://developer.android.com/reference/android/speech/RecognitionService, as updated from time to time, you acknowledge that the use of the API is subject to the Data Processing Addendum for Products where Google is a Data Processor, which is located at the following URL: https://privacy.google.com/businesses/gdprprocessorterms/, as updated from time to time. By clicking to accept, you hereby agree to the terms of the Data Processing Addendum for Products where Google is a Data Processor. + + +9. Terminating the License Agreement + +9.1 The License Agreement will continue to apply until terminated by either you or Google as set out below. + +9.2 If you want to terminate the License Agreement, you may do so by ceasing your use of the SDK and any relevant developer credentials. + +9.3 Google may at any time, terminate the License Agreement with you if: +(A) you have breached any provision of the License Agreement; or +(B) Google is required to do so by law; or +(C) the partner with whom Google offered certain parts of SDK (such as APIs) to you has terminated its relationship with Google or ceased to offer certain parts of the SDK to you; or +(D) Google decides to no longer provide the SDK or certain parts of the SDK to users in the country in which you are resident or from which you use the service, or the provision of the SDK or certain SDK services to you by Google is, in Google's sole discretion, no longer commercially viable. + +9.4 When the License Agreement comes to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the License Agreement has been in force) or which are expressed to continue indefinitely, shall be unaffected by this cessation, and the provisions of paragraph 14.7 shall continue to apply to such rights, obligations and liabilities indefinitely. + + +10. DISCLAIMER OF WARRANTIES + +10.1 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SDK IS AT YOUR SOLE RISK AND THAT THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND FROM GOOGLE. + +10.2 YOUR USE OF THE SDK AND ANY MATERIAL DOWNLOADED OR OTHERWISE OBTAINED THROUGH THE USE OF THE SDK IS AT YOUR OWN DISCRETION AND RISK AND YOU ARE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR COMPUTER SYSTEM OR OTHER DEVICE OR LOSS OF DATA THAT RESULTS FROM SUCH USE. + +10.3 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + + +11. LIMITATION OF LIABILITY + +11.1 YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO YOU UNDER ANY THEORY OF LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR EXEMPLARY DAMAGES THAT MAY BE INCURRED BY YOU, INCLUDING ANY LOSS OF DATA, WHETHER OR NOT GOOGLE OR ITS REPRESENTATIVES HAVE BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING. + + +12. Indemnification + +12.1 To the maximum extent permitted by law, you agree to defend, indemnify and hold harmless Google, its affiliates and their respective directors, officers, employees and agents from and against any and all claims, actions, suits or proceedings, as well as any and all losses, liabilities, damages, costs and expenses (including reasonable attorneys fees) arising out of or accruing from (a) your use of the SDK, (b) any application you develop on the SDK that infringes any copyright, trademark, trade secret, trade dress, patent or other intellectual property right of any person or defames any person or violates their rights of publicity or privacy, and (c) any non-compliance by you with the License Agreement. + + +13. Changes to the License Agreement + +13.1 Google may make changes to the License Agreement as it distributes new versions of the SDK. When these changes are made, Google will make a new version of the License Agreement available on the website where the SDK is made available. + + +14. General Legal Terms + +14.1 The License Agreement constitutes the whole legal agreement between you and Google and governs your use of the SDK (excluding any services which Google may provide to you under a separate written agreement), and completely replaces any prior agreements between you and Google in relation to the SDK. + +14.2 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the License Agreement (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google. + +14.3 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of the License Agreement is invalid, then that provision will be removed from the License Agreement without affecting the rest of the License Agreement. The remaining provisions of the License Agreement will continue to be valid and enforceable. + +14.4 You acknowledge and agree that each member of the group of companies of which Google is the parent shall be third party beneficiaries to the License Agreement and that such other companies shall be entitled to directly enforce, and rely upon, any provision of the License Agreement that confers a benefit on (or rights in favor of) them. Other than this, no other person or company shall be third party beneficiaries to the License Agreement. + +14.5 EXPORT RESTRICTIONS. THE SDK IS SUBJECT TO UNITED STATES EXPORT LAWS AND REGULATIONS. YOU MUST COMPLY WITH ALL DOMESTIC AND INTERNATIONAL EXPORT LAWS AND REGULATIONS THAT APPLY TO THE SDK. THESE LAWS INCLUDE RESTRICTIONS ON DESTINATIONS, END USERS AND END USE. + +14.6 The rights granted in the License Agreement may not be assigned or transferred by either you or Google without the prior written approval of the other party. Neither you nor Google shall be permitted to delegate their responsibilities or obligations under the License Agreement without the prior written approval of the other party. + +14.7 The License Agreement, and your relationship with Google under the License Agreement, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the License Agreement. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction. + + +January 16, 2019 + + + + 35 + 13 + true + + google_apis + Google APIs + + + google + Google Inc. + + x86_64 + + + 9 + + Google APIs Intel x86_64 Atom System Image + + + \ No newline at end of file diff --git a/测试相关资料/如何测试/本机测试环境/local_env.py b/测试相关资料/如何测试/本机测试环境/local_env.py new file mode 100644 index 0000000..0ed40bb --- /dev/null +++ b/测试相关资料/如何测试/本机测试环境/local_env.py @@ -0,0 +1,371 @@ +"""Rebuild project-owned Windows test environments; never connect to a device.""" +from __future__ import annotations + +import argparse +from contextlib import contextmanager +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import urllib.request +import zipfile +import xml.etree.ElementTree as ET + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +LOCAL = HERE / '.local' +CONFIG = LOCAL / 'tools.local.json' +PROFILES = ('ssh', 'image-editor', 'android') +IMAGE = HERE / 'android-image.json' + + +def run(command, *, env=None): + subprocess.run([str(p) for p in command], check=True, env=env) + + +def digest(path, algorithm='sha256'): + with path.open('rb') as stream: + return hashlib.file_digest(stream, algorithm).hexdigest() + + +def save_json(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') + + +def python_for(profile): + return LOCAL / profile / 'venv' / 'Scripts' / 'python.exe' + + +def installed(profile): + script = 'import importlib.metadata as m,json; print(json.dumps({d.metadata["Name"].lower().replace("_","-"):d.version for d in m.distributions()}))' + result = subprocess.run([str(python_for(profile)), '-c', script], check=True, + capture_output=True, text=True, encoding='utf-8') + return json.loads(result.stdout) + + +def validate_lock(profile): + source = ROOT / { + 'ssh': '移动端相关内容/安卓app/如何安卓测试/requirements-host.txt', + 'image-editor': '发布更新相关/镜像编辑器/编辑器源代码/requirements-build.txt', + }[profile] + lock = HERE / f'requirements-{profile}.lock.txt' + pins = {line.split(' --hash=', 1)[0].strip().lower().replace('_', '-') + for line in lock.read_text(encoding='utf-8').splitlines() + if line.strip() and not line.lstrip().startswith('#')} + for line in source.read_text(encoding='utf-8').splitlines(): + requirement = line.split('#', 1)[0].strip() + if requirement and requirement.lower().replace('_', '-') not in pins: + raise RuntimeError(f'{profile}: source requirements changed; update full dependency lock and wheel hashes') + + +def check_python(profile): + validate_lock(profile) + python = python_for(profile) + if not python.is_file(): + raise RuntimeError(f'{profile}: environment missing; run setup') + versions = installed(profile) + lock = HERE / f'requirements-{profile}.lock.txt' + for line in lock.read_text(encoding='utf-8').splitlines(): + if line and not line.startswith('#'): + name, version = line.split(' --hash=', 1)[0].split('==') + if versions.get(name.lower().replace('_', '-')) != version: + raise RuntimeError(f'{profile}: dependency version mismatch: {name}') + # A copied venv can appear functional but launchers still point to the previous location. + expected = str((LOCAL / profile / 'venv').resolve()) + cfg = (LOCAL / profile / 'venv' / 'pyvenv.cfg').read_text(encoding='utf-8') + if expected not in cfg: + raise RuntimeError(f'{profile}: copied or relocated venv; rename it and run setup again') + run([python, '-m', 'pip', 'check']) + print(f'{profile}: checked') + + +def setup_python(profile, offline): + validate_lock(profile) + folder = LOCAL / profile + venv = folder / 'venv' + lock = HERE / f'requirements-{profile}.lock.txt' + # Refuse to silently overwrite an existing broken/relocated environment. + if venv.exists(): + cfg = venv / 'pyvenv.cfg' + if not python_for(profile).is_file() or not cfg.is_file() or str(venv.resolve()) not in cfg.read_text(encoding='utf-8'): + raise RuntimeError(f'{profile}: existing venv is invalid; preserve/rename it before rebuilding') + else: + run([sys.executable, '-m', 'venv', venv]) + wheels = folder / 'wheels' + wheels.mkdir(parents=True, exist_ok=True) + if not offline: + print(f'{profile}: official PyPI wheels -> project-local cache; existing downloads/cache reused', flush=True) + run([sys.executable, '-m', 'pip', 'download', '--disable-pip-version-check', + '--index-url', 'https://pypi.org/simple', '--only-binary=:all:', '--require-hashes', + '--dest', wheels, '-r', lock]) + run([python_for(profile), '-m', 'pip', 'install', '--disable-pip-version-check', + '--no-index', '--find-links', wheels, '--require-hashes', '-r', lock]) + check_python(profile) + save_json(folder / 'wheel-sha256.json', {p.name: digest(p) for p in sorted(wheels.glob('*.whl'))}) + + +def tools(args=None): + config = json.loads(CONFIG.read_text(encoding='utf-8')) if CONFIG.is_file() else {} + for name in ('adb', 'emulator', 'jdk', 'sdk', 'gradle'): + supplied = getattr(args, name, None) if args else None + if supplied: + config[name] = str(supplied.resolve()) + # Prefer registered generic tools, then the normally installed Android Studio SDK. + home = Path.home() + sdk = Path(os.environ.get('LOCALAPPDATA', home / 'AppData/Local')) / 'Android/Sdk' + candidates = { + 'adb': [home / '.codex-tools/bin/platform-tools/adb.exe', sdk / 'platform-tools/adb.exe'], + 'emulator': [sdk / 'emulator/emulator.exe'], + 'sdk': [sdk], + 'jdk': [Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Android/Android Studio/jbr'], + 'gradle': [home / '.codex-tools/bin/gradle-9.5.0/bin/gradle.bat'], + } + for name, paths in candidates.items(): + if name not in config: + found = next((p for p in paths if p.exists()), None) + if found: + config[name] = str(found) + return config + + +def image_dir(): + return LOCAL / 'android/sdk/system-images/android-35/google_apis/x86_64' + + +def ini_values(path): + return {key.strip(): value.strip() for key, value in + (line.split('=', 1) for line in path.read_text(encoding='utf-8').splitlines() if '=' in line)} + + +def android_environment(): + env = os.environ.copy() + env['ANDROID_AVD_HOME'] = str(LOCAL / 'android/avd') + env['ANDROID_EMULATOR_HOME'] = str(LOCAL / 'android/emulator-home') + # The build SDK is a normally installed generic SDK; the test image is supplied via --sysdir. + config = tools() + if 'sdk' in config: + env['ANDROID_HOME'] = env['ANDROID_SDK_ROOT'] = config['sdk'] + if 'jdk' in config: + env['JAVA_HOME'] = config['jdk'] + return env + + +@contextmanager +def android_session(): + """Expose workspace-owned AVD/image through a temporary ASCII junction. + + Emulator's native file access cannot read the Chinese workspace path on + this host. The junction contains no data and is removed without traversal. + """ + spec = json.loads(IMAGE.read_text(encoding='utf-8')) + name = spec['avd'] + with tempfile.TemporaryDirectory(prefix='qms-local-emulator-') as temporary: + parent = Path(temporary) + if not str(parent).isascii(): + raise RuntimeError('Emulator needs an ASCII temporary directory; set TEMP for this process') + alias = parent / 'environment' + environment = os.environ.copy() + environment.update(QMS_ALIAS_PATH=str(alias), QMS_ALIAS_TARGET=str(LOCAL.resolve())) + script = '$ErrorActionPreference="Stop"; New-Item -ItemType Junction -Path $env:QMS_ALIAS_PATH -Target $env:QMS_ALIAS_TARGET | Out-Null' + subprocess.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', script], + env=environment, check=True, capture_output=True) + session = LOCAL / 'android' / ('session-' + parent.name) + try: + session.mkdir() + avd = alias / 'android/avd' / (name + '.avd') + (session / (name + '.ini')).write_text(f'avd.ini.encoding=UTF-8\npath={avd}\ntarget=android-35\n', encoding='utf-8') + launch_env = android_environment() + launch_env['ANDROID_AVD_HOME'] = str(alias / 'android' / session.name) + launch_env['ANDROID_EMULATOR_HOME'] = str(alias / 'android/emulator-home') + launch_env['ANDROID_USER_HOME'] = str(alias / 'android/user-home') + (LOCAL / 'android/user-home').mkdir(exist_ok=True) + yield launch_env, alias / image_dir().relative_to(LOCAL), alias + finally: + # Emulator writes its effective --sysdir back to config.ini. Restore + # the durable workspace spelling before deleting the temporary alias. + config_file = LOCAL / 'android/avd' / (name + '.avd') / 'config.ini' + values = ini_values(config_file) + values['image.sysdir.1'] = str(image_dir().resolve()) + config_file.write_text(''.join(f'{k}={v}\n' for k, v in values.items()), encoding='utf-8') + # Exact owned files only; never recursively delete via a junction. + index = session / (name + '.ini') + if index.is_file(): + index.unlink() + if session.exists(): + session.rmdir() + if alias.is_junction() and alias.resolve() == LOCAL.resolve(): + alias.rmdir() + elif alias.exists(): + raise RuntimeError('Temporary junction target changed; refusing cleanup') + + +def check_android(): + config = tools() + for key, suffix in (('adb', ''), ('emulator', ''), ('jdk', 'bin/java.exe'), ('sdk', 'platforms')): + if key not in config or not (Path(config[key]) / suffix).exists(): + raise RuntimeError(f'android: configure an installed generic {key} with setup --{key}') + directory = image_dir() + props = (directory / 'source.properties').read_text(encoding='utf-8') + for expected in ('Pkg.Revision=9', 'AndroidVersion.ApiLevel=35', 'SystemImage.Abi=x86_64', 'SystemImage.TagId=google_apis'): + if expected not in props.splitlines(): + raise RuntimeError('android: image version mismatch') + for name in ('package.xml', 'system.img', 'vendor.img', 'kernel-ranchu', 'ramdisk.img'): + if not (directory / name).is_file(): + raise RuntimeError(f'android: incomplete system image: {name}') + registration = ET.parse(directory / 'package.xml').getroot().find('localPackage') + spec = json.loads(IMAGE.read_text(encoding='utf-8')) + if registration is None or registration.get('path') != spec['package']: + raise RuntimeError('android: SDK package registration mismatch') + if registration.findtext('revision/major') != str(spec['revision']): + raise RuntimeError('android: SDK package revision mismatch') + name = json.loads(IMAGE.read_text(encoding='utf-8'))['avd'] + avd_home = LOCAL / 'android/avd' + index = avd_home / f'{name}.ini' + avd = avd_home / f'{name}.avd' + if not index.is_file() or f'path={avd.resolve()}' not in index.read_text(encoding='utf-8').splitlines(): + raise RuntimeError('android: stale AVD index; run setup android after copying the workspace') + if ini_values(avd / 'config.ini').get('image.sysdir.1') != str(directory.resolve()): + raise RuntimeError('android: stale AVD system image path; run setup android') + result = subprocess.run([config['emulator'], '-list-avds'], env=android_environment(), + check=True, capture_output=True, text=True, encoding='utf-8') + if name not in result.stdout.splitlines(): + raise RuntimeError('android: Emulator cannot discover project AVD') + print('android: image package and project AVD checked; no device accessed') + + +def setup_android(args): + config = tools(args) + save_json(CONFIG, config) + spec = json.loads(IMAGE.read_text(encoding='utf-8')) + directory = image_dir() + if not directory.exists(): + archive = args.image_archive + if archive is None: + if args.offline: + raise RuntimeError('android: missing image; provide --image-archive with the pinned official ZIP') + archive = LOCAL / 'work/downloads' / Path(spec['url']).name + archive.parent.mkdir(parents=True, exist_ok=True) + if not archive.exists(): + print(f'Missing API 35 revision 9 image: downloading official Google ZIP from {spec["url"]}\n' + 'Destination: project .local/work/downloads; no existing file will be overwritten.', flush=True) + partial = archive.with_suffix('.partial') + # A failed download is retried only by a subsequent explicit setup invocation. + with urllib.request.urlopen(spec['url'], timeout=60) as source, partial.open('xb') as out: + shutil.copyfileobj(source, out) + partial.rename(archive) + if digest(archive, 'sha1') != spec['sha1']: + raise RuntimeError('android: official archive checksum mismatch') + save_json(LOCAL / 'android/archive-sha256.json', {'file': archive.name, 'sha256': digest(archive)}) + staging = LOCAL / 'android/image-staging' + if staging.exists(): + raise RuntimeError('android: previous staging exists; inspect it before retrying') + staging.mkdir(parents=True) + with zipfile.ZipFile(archive) as bundle: + for member in bundle.infolist(): + target = (staging / member.filename).resolve() + if not target.is_relative_to(staging.resolve()): + raise RuntimeError('android: unsafe archive member') + bundle.extractall(staging) + candidates = list(staging.glob('*/source.properties')) + if len(candidates) != 1: + raise RuntimeError('android: unexpected official archive layout') + extracted = candidates[0].parent + # ZIPs may omit SDK Manager's local registration. Preserve the pinned official descriptor. + if not (extracted / 'package.xml').exists(): + shutil.copyfile(HERE / 'android-package.xml', extracted / 'package.xml') + directory.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(extracted), directory) + staging.rmdir() + avd_home = LOCAL / 'android/avd' + avd = avd_home / (spec['avd'] + '.avd') + index = avd_home / (spec['avd'] + '.ini') + if avd.exists() and not (avd / '.qms-managed').is_file(): + raise RuntimeError('android: refusing to overwrite an unmanaged AVD') + if index.exists() and not avd.exists(): + raise RuntimeError('android: orphan index exists; preserve it before setup') + avd.mkdir(parents=True, exist_ok=True) + (avd / '.qms-managed').touch() + values = { + 'avd.ini.encoding': 'UTF-8', 'AvdId': spec['avd'], 'avd.ini.displayname': spec['avd'], + 'hw.cpu.arch': 'x86_64', 'hw.cpu.ncore': '2', 'hw.ramSize': '2048', + 'hw.lcd.width': '1080', 'hw.lcd.height': '1920', 'hw.lcd.density': '420', + 'hw.gpu.enabled': 'yes', 'hw.gpu.mode': 'auto', 'hw.keyboard': 'yes', + 'hw.camera.back': 'none', 'hw.camera.front': 'none', 'hw.audioInput': 'no', + 'disk.dataPartition.size': '2G', 'image.sysdir.1': str(directory.resolve()), + 'tag.id': 'google_apis', 'tag.display': 'Google APIs', 'abi.type': 'x86_64', + 'PlayStore.enabled': 'false', 'fastboot.forceColdBoot': 'yes', + } + # On copied workspaces refresh only location keys; preserve user changes and userdata. + cfg = avd / 'config.ini' + if cfg.is_file(): + values = ini_values(cfg) + values['image.sysdir.1'] = str(directory.resolve()) + cfg.write_text(''.join(f'{k}={v}\n' for k, v in values.items()), encoding='utf-8') + index.write_text(f'avd.ini.encoding=UTF-8\npath={avd.resolve()}\ntarget=android-35\n', encoding='utf-8') + (LOCAL / 'android/emulator-home').mkdir(exist_ok=True) + check_android() + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('action', choices=('setup', 'check', 'build-ui', 'ui-smoke')) + parser.add_argument('profile', choices=PROFILES) + parser.add_argument('--offline', action='store_true') + parser.add_argument('--image-archive', type=Path) + for name in ('adb', 'emulator', 'jdk', 'sdk', 'gradle'): + parser.add_argument('--' + name, type=Path) + parser.add_argument('--build', type=Path, help='Existing isolated build containing uiTest-debug.apk; ui-smoke only') + parser.add_argument('--report', type=Path, help='New result JSON; ui-smoke only') + args = parser.parse_args(argv) + try: + if os.name != 'nt': + raise RuntimeError('These pinned environments target Windows x64; use the platform-specific build instructions elsewhere') + if args.profile != 'android' and sys.version_info[:2] != (3, 14): + raise RuntimeError('Pinned Windows wheels require CPython 3.14 x64') + if args.action == 'build-ui': + if args.profile != 'android': + parser.error('build-ui requires android') + check_android() + config = tools() + build_root = LOCAL / 'work/android-build' + build_root.mkdir(parents=True, exist_ok=True) + before = set(build_root.glob('qms-build-*')) + builder = ROOT / '移动端相关内容/安卓app/构建与发布/build_android.py' + with android_session() as (launch_env, _, alias): + command = [sys.executable, builder, '--work-root', alias / 'work/android-build', + '--jdk', config['jdk'], '--sdk', config['sdk']] + if config.get('gradle'): + command += ['--gradle', config['gradle']] + run([*command, ':uiTest:assembleDebug'], env=launch_env) + for output in sorted(set(build_root.glob('qms-build-*')) - before): + print(f'Project-local build: {output.relative_to(ROOT)}') + elif args.action == 'ui-smoke': + if args.profile != 'android' or not args.build or not args.report: + parser.error('ui-smoke android requires --build and --report') + check_android() + config = tools() + name = json.loads(IMAGE.read_text(encoding='utf-8'))['avd'] + runner = ROOT / '移动端相关内容/安卓app/如何安卓测试/run_emulator_ui_test.py' + with android_session() as (launch_env, system_image, _): + run([sys.executable, runner, '--scenario', 'settings', '--avd', name, + '--adb', config['adb'], '--emulator', config['emulator'], '--sysdir', system_image, + '--build', args.build.resolve(), '--report', args.report.resolve()], env=launch_env) + elif args.profile == 'android': + setup_android(args) if args.action == 'setup' else check_android() + elif args.action == 'setup': + setup_python(args.profile, args.offline) + else: + check_python(args.profile) + return 0 + except (OSError, RuntimeError, subprocess.CalledProcessError, ValueError) as error: + print(f'Local environment failed: {error}', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/测试相关资料/如何测试/本机测试环境/requirements-image-editor.lock.txt b/测试相关资料/如何测试/本机测试环境/requirements-image-editor.lock.txt new file mode 100644 index 0000000..e267a4e --- /dev/null +++ b/测试相关资料/如何测试/本机测试环境/requirements-image-editor.lock.txt @@ -0,0 +1,10 @@ +# CPython 3.14 / Windows x64; official PyPI wheels, full dependency closure. +altgraph==0.17.5 --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 +packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +pefile==2024.8.26 --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f +PyInstaller==6.22.2 --hash=sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7 +pyinstaller-hooks-contrib==2026.7 --hash=sha256:24257a04c7a5a7a034cf28e39dcee20fbeeb9f043076729480f2e1b69904408a +PySide6-Essentials==6.11.2 --hash=sha256:c8a29def77032773a30879f7f24415b5395ad08592d147c170824ef4c735dfc1 +pywin32-ctypes==0.2.3 --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 +setuptools==84.0.0 --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 +shiboken6==6.11.2 --hash=sha256:6ab0eba1c904455df621f9a6df3ca2bb896bab8670572d2bc4e37804ae91f19a diff --git a/测试相关资料/如何测试/本机测试环境/requirements-ssh.lock.txt b/测试相关资料/如何测试/本机测试环境/requirements-ssh.lock.txt new file mode 100644 index 0000000..2876a35 --- /dev/null +++ b/测试相关资料/如何测试/本机测试环境/requirements-ssh.lock.txt @@ -0,0 +1,8 @@ +# CPython 3.14 / Windows x64; official PyPI wheels, full dependency closure. +paramiko==4.0.0 --hash=sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9 +bcrypt==5.0.0 --hash=sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2 +cffi==2.1.1 --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 +cryptography==50.0.1 --hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 +invoke==3.0.3 --hash=sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053 +pycparser==3.0 --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 +PyNaCl==1.6.2 --hash=sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 diff --git a/测试相关资料/如何测试/本机测试环境/test_local_env.py b/测试相关资料/如何测试/本机测试环境/test_local_env.py new file mode 100644 index 0000000..fb8b459 --- /dev/null +++ b/测试相关资料/如何测试/本机测试环境/test_local_env.py @@ -0,0 +1,107 @@ +"""Host-only relocation and preservation checks; no network, devices or installs.""" +import importlib.util +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import local_env as env + + +class LocalEnvironmentTests(unittest.TestCase): + def test_workspace_location_uses_source_not_cwd(self): + self.assertEqual(env.ROOT / '测试相关资料/如何测试/本机测试环境', env.HERE) + module = env.ROOT / '移动端相关内容/安卓app/如何安卓测试/local_test_paths.py' + spec = importlib.util.spec_from_file_location('test_paths', module) + value = importlib.util.module_from_spec(spec) + spec.loader.exec_module(value) + self.assertEqual(value.SSH_KNOWN_HOSTS, env.LOCAL / 'private/qms-mobile-known_hosts') + + def test_copied_venv_is_rejected_without_overwriting(self): + with tempfile.TemporaryDirectory(prefix='中文 relocation ') as tmp: + local = Path(tmp) + venv = local / 'ssh/venv' + (venv / 'Scripts').mkdir(parents=True) + (venv / 'Scripts/python.exe').touch() + cfg = venv / 'pyvenv.cfg' + cfg.write_text('command = python -m venv old-location\n', encoding='utf-8') + with patch.object(env, 'LOCAL', local), patch.object(env, 'run', side_effect=AssertionError('unexpected install')): + with self.assertRaisesRegex(RuntimeError, 'preserve/rename'): + env.setup_python('ssh', offline=True) + self.assertEqual(cfg.read_text(encoding='utf-8'), 'command = python -m venv old-location\n') + + def test_source_dependency_change_requires_a_new_lock(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + source = root / '移动端相关内容/安卓app/如何安卓测试/requirements-host.txt' + source.parent.mkdir(parents=True) + source.write_text('paramiko==5.0.0\n', encoding='utf-8') + (root / 'requirements-ssh.lock.txt').write_text('paramiko==4.0.0 --hash=sha256:old\n', encoding='utf-8') + with patch.object(env, 'ROOT', root), patch.object(env, 'HERE', root): + with self.assertRaisesRegex(RuntimeError, 'source requirements changed'): + env.validate_lock('ssh') + + def test_offline_missing_image_does_not_download(self): + with tempfile.TemporaryDirectory(prefix='中文 offline ') as tmp: + local = Path(tmp) + args = type('Args', (), {'offline': True, 'image_archive': None})() + with patch.object(env, 'LOCAL', local), patch.object(env, 'CONFIG', local / 'tools.local.json'), patch('urllib.request.urlopen', side_effect=AssertionError('network')): + with self.assertRaisesRegex(RuntimeError, 'missing image'): + env.setup_android(args) + + def test_stale_avd_can_refresh_paths_without_resetting_userdata(self): + with tempfile.TemporaryDirectory(prefix='中文 avd ') as tmp: + local = Path(tmp) + directory = local / 'android/sdk/system-images/android-35/google_apis/x86_64' + directory.mkdir(parents=True) + avd = local / 'android/avd/qms-local-api35.avd' + avd.mkdir(parents=True) + (avd / '.qms-managed').touch() + (avd / 'config.ini').write_text('hw.ramSize=3072\nimage.sysdir.1=old\n', encoding='utf-8') + userdata = avd / 'userdata-qemu.img' + userdata.write_bytes(b'preserve') + args = type('Args', (), {'offline': True, 'image_archive': None})() + with patch.object(env, 'LOCAL', local), patch.object(env, 'CONFIG', local / 'tools.local.json'), patch.object(env, 'check_android'): + env.setup_android(args) + config = (avd / 'config.ini').read_text(encoding='utf-8') + self.assertIn('hw.ramSize=3072', config) + self.assertIn(str(directory.resolve()), config) + self.assertEqual(userdata.read_bytes(), b'preserve') + + def test_unmanaged_avd_is_never_overwritten(self): + with tempfile.TemporaryDirectory() as tmp: + local = Path(tmp) + (local / 'android/sdk/system-images/android-35/google_apis/x86_64').mkdir(parents=True) + avd = local / 'android/avd/qms-local-api35.avd' + avd.mkdir(parents=True) + cfg = avd / 'config.ini' + cfg.write_text('owner=someone-else\n', encoding='utf-8') + args = type('Args', (), {'offline': True, 'image_archive': None})() + with patch.object(env, 'LOCAL', local), patch.object(env, 'CONFIG', local / 'tools.local.json'): + with self.assertRaisesRegex(RuntimeError, 'unmanaged'): + env.setup_android(args) + self.assertEqual(cfg.read_text(encoding='utf-8'), 'owner=someone-else\n') + + @unittest.skipUnless(__import__('os').name == 'nt', 'Windows junction lifecycle') + def test_emulator_session_restores_image_path_and_removes_only_alias(self): + with tempfile.TemporaryDirectory(prefix='中文 session ') as tmp: + local = Path(tmp) + avd = local / 'android/avd/qms-local-api35.avd' + avd.mkdir(parents=True) + cfg = avd / 'config.ini' + cfg.write_text('hw.ramSize = 3072\nimage.sysdir.1 = before\n', encoding='utf-8') + (local / 'android/emulator-home').mkdir() + with patch.object(env, 'LOCAL', local): + with env.android_session() as (launch, image, alias): + self.assertTrue(alias.is_junction()) + self.assertTrue(str(image).isascii()) + self.assertTrue(str(launch['ANDROID_AVD_HOME']).isascii()) + cfg.write_text(f'hw.ramSize = 3072\nimage.sysdir.1 = {image}\n', encoding='utf-8') + self.assertFalse(alias.exists()) + self.assertEqual(env.ini_values(cfg)['image.sysdir.1'], str(env.image_dir().resolve())) + self.assertEqual(env.ini_values(cfg)['hw.ramSize'], '3072') + self.assertTrue(avd.is_dir()) + + +if __name__ == '__main__': + unittest.main() diff --git a/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md b/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md index 3ff5d2a..bb5c65e 100644 --- a/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md +++ b/测试相关资料/如何测试/核桃派点阵屏控制服务测试流程.md @@ -54,6 +54,7 @@ NetworkManager DNS 往返:`tests/test_wifi.py::test_nmcli_multiple_dns_can_rou | `TEST-LOW-VOLTAGE` | 限亮、图标、恢复 | 实屏 + ADC + 可调电源 | 每一档停顿 | | `TEST-CLEANUP` | 临时项、持久数据、GPIO 安全状态 | 本机和核桃派 | 否 | | `TEST-INDEPENDENCE` | 单目录可拿走、无外部依赖 | 隔离临时副本 | 否 | +| `TEST-LOCAL-ENV` | `DEPLOY-WORKSPACE-LAYOUT` 项目专用环境、路径刷新和 Git 边界 | Windows 本机及中文/空格隔离副本 | 否 | ## 1. 本机 mock 与自动化 @@ -243,6 +244,8 @@ nmcli -t -f GENERAL.STATE,IP4.ADDRESS,IP4.GATEWAY device show wlan0 ## 9. 清理与最终独立性 +`TEST-LOCAL-ENV` 按 [本机环境说明](本机测试环境/README.md) 的 setup/check、主机单元测试与隔离复建命令验证:锁定 wheel 安装及依赖检查通过,迁移保护和场景选择测试通过;项目 AVD 包登记有效,settings/baseline 冒烟通过并恢复环境;中文及空格的新位置不复制 venv/私有登记也能复建;`.local/` 被 Git 忽略,卫生检查通过。缺少实际验证时记“未验证”。此项不触发真机、SSH、切网、摄像头或实屏测试。迁移结束按明确路径移除旧项目专用目录和工具登记,通用工具保留。 + - 停止所有临时 unit,按已验证的明确绝对路径删除临时源码、venv、wheelhouse、构建和测试数据;保留 `/var/lib/matrix-screen-controller`。 - 复核 GPIO 安全状态、系统 `running`、生产服务健康、无临时端口和无残留诊断环境变量。 - 独立目录不得含 `.venv`、缓存、运行数据、旧设备配置、旧凭据、主机密钥或外层绝对路径。 diff --git a/移动端相关内容/AGENTS.md b/移动端相关内容/AGENTS.md index b1c6234..62b5417 100644 --- a/移动端相关内容/AGENTS.md +++ b/移动端相关内容/AGENTS.md @@ -16,6 +16,7 @@ - 设备持久 ID、协议能力和业务命令是连接契约,不依赖蓝牙 MAC、网页布局或私有驱动调用。 - 只产出 debug APK,正式签名必须有用户明确指令;调试密钥也不进入 Git。 - 源码、测试、发布说明都在安卓app下;构建缓存、SDK、密钥、真实设备登记不进 Git。 +- 项目专用 SSH venv、测试 SDK、AVD 与主机密钥统一存放在根目录的 `测试相关资料/如何测试/本机测试环境/.local/`;通用工具保持复用。搭建入口、锁文件和测试经验跟踪提交,具体命令见该目录 README,禁止新增全局项目专用测试材料。 - Android Studio 属于正常安装软件:下载官方安装包、校验、交给用户安装后停下等待,不安装进 Codex 工具目录。 - 不擅自关闭 VPN、不自动修改系统设置以修复下载。版本、依赖和 Wrapper 必须锁定并可复建。 diff --git a/移动端相关内容/安卓app/如何安卓测试/README.md b/移动端相关内容/安卓app/如何安卓测试/README.md index 92b4f88..ae552a5 100644 --- a/移动端相关内容/安卓app/如何安卓测试/README.md +++ b/移动端相关内容/安卓app/如何安卓测试/README.md @@ -8,6 +8,8 @@ WiFi 加速与回退专项见 [WiFi 传输测试](WiFi传输测试.md),必须 远端联调脚本在电脑端使用 `requirements-host.txt` 的 Paramiko;它不属于核桃派离线安装依赖,不能加入设备端 `requirements-dev.txt` 或 OTA wheelhouse。 +项目 SSH 环境、独立测试 SDK、AVD 和私有主机密钥统一放在仓库内被忽略的本机 `.local/`,不再写入全局工具目录。复建、版本锁定与中文路径模拟器入口见 [本机环境说明](../../../测试相关资料/如何测试/本机测试环境/README.md)。SSH 用其中 `.local/ssh/venv/Scripts/python.exe` 运行本目录脚本;凭据/登记门禁不变。普通 UI 使用 `local_env.py build-ui android` 和 `ui-smoke android` 做定向验证,不涉及真机或核桃派。 + ## 1. 门禁 - 用户授权主测试机可安装覆盖本 App、清理 App 测试数据、操作权限及蓝牙;自动测试点击使用 root,也不操作其他应用数据。 diff --git a/移动端相关内容/安卓app/如何安卓测试/local_test_paths.py b/移动端相关内容/安卓app/如何安卓测试/local_test_paths.py new file mode 100644 index 0000000..897f6fe --- /dev/null +++ b/移动端相关内容/安卓app/如何安卓测试/local_test_paths.py @@ -0,0 +1,12 @@ +"""Workspace-owned private host-test state; importing this performs no I/O.""" +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +LOCAL = ROOT / '测试相关资料/如何测试/本机测试环境/.local' +PRIVATE = LOCAL / 'private' +SSH_KNOWN_HOSTS = PRIVATE / 'qms-mobile-known_hosts' + + +def private_directory(): + PRIVATE.mkdir(parents=True, exist_ok=True) + return PRIVATE diff --git a/移动端相关内容/安卓app/如何安卓测试/remote_support.py b/移动端相关内容/安卓app/如何安卓测试/remote_support.py index 63fcde7..be9d035 100644 --- a/移动端相关内容/安卓app/如何安卓测试/remote_support.py +++ b/移动端相关内容/安卓app/如何安卓测试/remote_support.py @@ -7,6 +7,7 @@ from pathlib import Path import re import paramiko +from local_test_paths import SSH_KNOWN_HOSTS ROOT = Path(__file__).resolve().parents[3] @@ -23,7 +24,7 @@ def connect(): host = fields["IP"].strip() port_match = re.search(r"(?:^|\s)-p\s+(\d+)", fields["SSH"]) port = int(port_match.group(1)) if port_match else 22 - trust = Path.home() / ".codex-tools/tmp/qms-mobile-known_hosts" + trust = SSH_KNOWN_HOSTS trust.parent.mkdir(parents=True, exist_ok=True) client = paramiko.SSHClient() if trust.exists(): diff --git a/移动端相关内容/安卓app/如何安卓测试/run_wifi_switch_test.py b/移动端相关内容/安卓app/如何安卓测试/run_wifi_switch_test.py index 2766d44..5bf9239 100644 --- a/移动端相关内容/安卓app/如何安卓测试/run_wifi_switch_test.py +++ b/移动端相关内容/安卓app/如何安卓测试/run_wifi_switch_test.py @@ -321,7 +321,8 @@ def main() -> int: raise RuntimeError("当前设备地址不是电脑热点网段") import paramiko keys = paramiko.HostKeys() - keys.load(str(Path.home() / ".codex-tools/tmp/qms-mobile-known_hosts")) + from local_test_paths import SSH_KNOWN_HOSTS + keys.load(str(SSH_KNOWN_HOSTS)) expected_key = next(iter(keys.lookup(fields["IP"].strip()).values())) board = connect_at(current_ip, expected_key, fields) original, backup_root, backup_unit = resume_configuration(board, fields) @@ -355,8 +356,8 @@ def main() -> int: "static_address": static_address, "hotspot_gateway": hotspot["gateway"], "wrong_password": "qms-invalid-password-2026", } - private_tmp = Path.home() / ".codex-tools/tmp" - private_tmp.mkdir(parents=True, exist_ok=True) + from local_test_paths import private_directory + private_tmp = private_directory() path = private_tmp / ("qms-wifi-test-" + secrets.token_hex(8) + ".json") path.write_text(json.dumps(values, ensure_ascii=False), encoding="utf-8") host_secret = path diff --git a/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md b/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md index a319e3e..12493bb 100644 --- a/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md +++ b/移动端相关内容/安卓app/如何安卓测试/常见问题与排障.md @@ -82,7 +82,7 @@ Compose 的禁用语义可能位于父节点,文字节点自身仍 enabled。 ## 13. 按影响选测与独立模拟器(2026-09-29) - 先读 [按影响选测](按影响选测.md),选中场景后才执行环境门禁。不能因为手机登记/网络不可用而阻塞不依赖它们的 JVM、模拟器或文档检查。需要真机但环境不可用时仍是“未验证”。 -- `--list-scenarios` / `--dry-run` 不导入远端联调依赖、不访问凭据或 ADB。电脑系统 Python 未装 Paramiko 时,优先复用本机测试环境的 `.venv` 并检查 import;不要给核桃派添加主机依赖。 +- `--list-scenarios` / `--dry-run` 不导入远端联调依赖、不访问凭据或 ADB。电脑系统 Python 未装 Paramiko 时,使用本机 `local_env.py setup ssh` 重建 `.local/ssh/venv` 并检查 import;不要给核桃派添加主机依赖。 - com.android.test 插件已由同版本 AGP 放入 classpath 时,uiTest 使用无版本 `id("com.android.test")`;显式重复版本可能导致 unknown version 冲突。uiTest 的 Compose 测试库与现有 Compose Multiplatform 锁定版本一致。 - 独立 uiTest 使用 self-instrumenting APK 和实际 Controller,不从反射篡改内部状态。fake RPC 必须遵循真实字段契约:内容条目需要 revision,缺失会在 thumbnailKey 中失败。此类夹具错误应修夹具,不弱化产品断言。 - 真机 navigation 临时撤销扫描/连接权限,防止已有记忆触发自动重连;finally 恢复原权限并回读,不访问板端身份。不将此场景用于权限允许/拒绝的产品验收。 @@ -94,3 +94,14 @@ Compose 的禁用语义可能位于父节点,文字节点自身仍 enabled。 真实用户动画的首帧可能是 `path` 指向持久图片,演示动画则可能是内嵌 `image`。新增 BLE 图片接口时要先检查 `animations.playback_snapshot` 的实际首帧结构,并沿设备既有路径验证与解码规则读取两种形式;不能把 `path` 当作无图或错误参数。空动画仍应按不可播放条目处理。复测须覆盖用户动画和演示动画,并做全库图片只读读取;本轮修正和实测见 [内容缓存验收](../测试结果归档/2026-09-29_内容缓存/README.md)。 模拟器 AVD 名称若存在但启动提示系统目录不存在,先核对该名称的配置指向和镜像登记。保留旧配置,使用已安装镜像新建专用 AVD 再测试;不要因名称存在就反复启动,也不要擅自清空原 AVD。真机 instrumentation 前同时确认产品 APK 与 androidTest APK 已构建,只有测试 APK 时先补产品构建,再运行选中场景。 + +## 15. 项目本机环境迁移与中文路径(2026-10-01) + +- 本项目专用 venv、AVD/系统镜像和私有主机状态使用 [本机环境入口](../../../测试相关资料/如何测试/本机测试环境/README.md),不再依赖全局项目目录。旧全局 AVD 索引存在不代表 AVD 数据存在,先检查实际目录;缺失的数据和主机密钥不能写成迁移成功。 +- venv 不能直接搬动后作为可移植环境:`pyvenv.cfg` 和入口脚本可能仍引用原位置。保留旧目录,用完整版本/哈希锁定 wheel 在新位置重建,再检查依赖与解释器。 +- 本机 Emulator 在中文路径下报告 `Could not write file`、损坏的中文路径和无法读取 kernel;同一系统镜像通过 ASCII 目录联接启动后 settings/baseline 通过,确认本次启动失败属于路径访问问题。用 `local_env.py ui-smoke android` 自动创建短期入口,镜像/AVD 留在项目内,不移动到全局目录或再次下载镜像。临时入口结束后移除,AVD 的持久 image 路径恢复为项目内位置。 +- AGP 构建入口传入 ASCII 目录联接时,不应先 `resolve()` 展开为中文目标;`build_android.py` 保留用户传入的 ASCII 拼写,构建实际写入项目 `.local/work/`。只读路径解析使用常规 resolve,不把此规则扩展到其他环境。 +- Emulator 可能把 `config.ini` 重写成 `key = value`,读取时应修剪键值空白,不能把格式变化误报为失效路径。复制项目后 setup 只刷新本工具管理的路径,保留用户数据。 +- Windows Python 控制台在诊断输出包含替换字符时可能触发 GBK 编码错误;仅对本次 Python 进程使用 `-X utf8`,不全局调整系统设置。 + +单次启动失败与复测结果记录在 `各种归档/2026-10-01_本机环境迁移/README.md`;模拟器结果不代表任何真机或板端验收。 diff --git a/移动端相关内容/安卓app/构建与发布/README.md b/移动端相关内容/安卓app/构建与发布/README.md index dfc343a..aaf1302 100644 --- a/移动端相关内容/安卓app/构建与发布/README.md +++ b/移动端相关内容/安卓app/构建与发布/README.md @@ -29,6 +29,8 @@ python "移动端相关内容/安卓app/构建与发布/build_android.py" --work 路径实测:AGP 默认拒绝非 ASCII 路径;启用 `android.overridePathCheck=true` 后编译继续,但当前 JBR/Gradle 下 JVM 测试出现类加载失败。在 ASCII 隔离目录同一测试通过,故当前使用该回退。2026-09-25 已在全新且包含空格的 ASCII 构建根目录重新执行全部 92 个任务,JVM 测试及两个 debug APK 均成功;保留中文源码位置。 +项目专用 UI 构建现使用 [本机环境入口](../../../测试相关资料/如何测试/本机测试环境/README.md) 的 `local_env.py build-ui android`:缓存与构建实际保存在项目 `.local/work/`,短期 ASCII 目录联接只作调用入口并在结束后移除。`build_android.py --work-root` 保留显式 ASCII 联接的拼写,避免先 resolve 展开为中文路径。产品 APK 原有版本、签名和交付流程不变。 + ## APK 与签名 - 应用名“奇妙小屏幕”,applicationId 为 `org.qimiaoscreen.controller`,minSdk 26,首版版本 0.1.0。 diff --git a/移动端相关内容/安卓app/构建与发布/build_android.py b/移动端相关内容/安卓app/构建与发布/build_android.py index 2ab7d84..5250485 100644 --- a/移动端相关内容/安卓app/构建与发布/build_android.py +++ b/移动端相关内容/安卓app/构建与发布/build_android.py @@ -17,7 +17,9 @@ def main(): parser.add_argument('--gradle', type=Path, help='Optional verified Gradle executable; otherwise use Wrapper') parser.add_argument('tasks', nargs='*', default=[':sharedCore:jvmTest', ':androidApp:assembleDebug']) args = parser.parse_args() - root = args.work_root.resolve() + # Keep an explicitly supplied ASCII junction spelling. Resolving it first + # would expose the Chinese workspace path to AGP despite the alias. + root = Path(os.path.abspath(args.work_root)) if not str(root).isascii(): parser.error('work-root must use ASCII characters') if not (args.jdk / 'bin/java.exe').is_file() or not (args.sdk / 'platforms').is_dir(): diff --git a/移动端相关内容/开发要求/跨端兼容与变更记录.md b/移动端相关内容/开发要求/跨端兼容与变更记录.md index 3fa1fd9..d2f0ba1 100644 --- a/移动端相关内容/开发要求/跨端兼容与变更记录.md +++ b/移动端相关内容/开发要求/跨端兼容与变更记录.md @@ -72,3 +72,7 @@ MOBILE-STATUS/DISCOVERY/PREVIEW:顶栏自动循环、扫描条目断开与 DEVICE-MOBILE-LIBRARY-CACHE/MOBILE-LIBRARY-CACHE:设备协议 1.1 在主版本 1 下新增 `library_progressive`、`library.summary` 和 16×16 `library.preview`,保留既有 64×64 `library.thumbnail` 与旧目录契约。Android 0.2.2 使用分档缓存和渐进加载,旧设备回退现有方式;旧 App 可继续连接新设备。设备与 App 本轮均获用户明确迭代和部署授权。验证证据待本轮测试归档,不涉及驱动或当前帧输出。 兼容说明:原 mobile.transport 为 BLE 统计对象,保持原义;实际链路新增 active_transport,不改变旧字段类型。 + +## 2026-10-01 本机测试环境归属 + +项目专用 SSH/编辑器环境、Android 测试系统镜像与 AVD 迁入本机测试目录 `.local/`,搭建入口及锁定依赖进入源码管理,私有主机状态/临时材料不提交。Android 构建入口支持指向项目存储的短期 ASCII 联接;SSH 和 WiFi 测试私有路径改为工作区内。只改变测试基础设施;App、设备公共接口、协议、驱动和部署契约均不变,不发布产品 APK、不部署设备、不推进版本和 FEATURE_UPDATED_AT。