Move project test environments into the workspace
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
.venv/
|
||||
.local/
|
||||
data/
|
||||
work/
|
||||
__pycache__/
|
||||
|
||||
@@ -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 "<adb.exe>" --emulator "<emulator.exe>" --jdk "<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、真机、板端或实屏验收通过。
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<ns2:repository xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns2="http://schemas.android.com/repository/android/common/02" xmlns:sys-img="http://schemas.android.com/sdk/android/repo/sys-img2/03"><license id="android-sdk-license" type="text">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
|
||||
</license>
|
||||
<localPackage path="system-images;android-35;google_apis;x86_64">
|
||||
<type-details xsi:type="sys-img:sysImgDetailsType">
|
||||
<api-level>35</api-level>
|
||||
<extension-level>13</extension-level>
|
||||
<base-extension>true</base-extension>
|
||||
<tag>
|
||||
<id>google_apis</id>
|
||||
<display>Google APIs</display>
|
||||
</tag>
|
||||
<vendor>
|
||||
<id>google</id>
|
||||
<display>Google Inc.</display>
|
||||
</vendor>
|
||||
<abi>x86_64</abi>
|
||||
</type-details>
|
||||
<revision>
|
||||
<major>9</major>
|
||||
</revision>
|
||||
<display-name>Google APIs Intel x86_64 Atom System Image</display-name>
|
||||
<uses-license ref="android-sdk-license" />
|
||||
</localPackage>
|
||||
</ns2:repository>
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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`、缓存、运行数据、旧设备配置、旧凭据、主机密钥或外层绝对路径。
|
||||
|
||||
Reference in New Issue
Block a user