Files
ShadowsocksX-NG/docs/development.zh.md

11 KiB
Raw Permalink Blame History

开发、构建与测试

English | 返回工程首页

除特别说明外,命令从 ShadowsocksX-NG2/ 执行,文中工程路径也相对于该目录。

统一验证

统一验证入口(从仓库根目录调用):

task --exit-code --taskfile ShadowsocksX-NG2/Taskfile.dist.yml validate
# 按指定顺序只运行所需关卡:
task --exit-code --taskfile ShadowsocksX-NG2/Taskfile.dist.yml validate -- format:check lint test

在工程目录内可用 task --exit-code validate。从任意目录可用 task --exit-code --taskfile /path/to/ShadowsocksX-NG/ShadowsocksX-NG2/Taskfile.dist.yml validate。入口按 Taskfile 所在位置定位工程目录,内部调用发行 Taskfile 中的单项任务。

默认按 lint → test → build 执行;真实进程冒烟测试仍可显式选择 test:smoke。格式检查不属于默认验证循环:本地由 pre-commit 控制格式;CI 在 Xcode 27 arm64 runner 上单独运行 task format:check,与 Swift 6.4 格式工具基线一致。也可按指定顺序选择子集,每关最多一次;任一关失败立即停止,后续关标记 not_run,进程返回非零。--exit-code 让 Task 透传关卡退出码;省略时 Task 仍返回非零,但可能使用自己的错误码。入口不自动重试,不修改测试并行设置,也不执行会修改源码的格式化。

验证产物与证据

每次运行创建独立的 .build/validation/<UTC时间>-<随机后缀>/,开始与结束均输出结果绝对路径。summary.txt 保存各关状态、退出码与证据状态;每个已执行关卡在同名子目录下保留合并 stdout/stderr 的 raw.log 和 status.txt(test:smoke 对应 test-smoke/),Xcode 关卡显式保存 result.xcresult。CI artifact 上传摘要、状态、日志和测试计数诊断文件,不上传 .xcresult 结果包,以减少下载体积;完整结果包在 job 运行期间仍保留于 runner 工作区。本地验证仍在验证目录保留全部证据。控制台只显示进度和摘要,需要完整错误时直接读取对应日志。所有产物均在现有忽略目录内;task clean 会删除它们,需要长期留存时先复制出去。

测试关通过本机 xcrun xcresulttool get test-results summary 和系统 plutil 尽力读取计数,无需新增脚本或工具依赖。test-summary.json 保存原始摘要,test-summary.stderr.log 保存 xcresulttool 错误,count-reader.log 保存计数读取错误。汇总展示通过、失败、跳过、预期失败及总数(Xcode 汇总口径,参数化测试的实际执行次数可能不同)。关卡与总体退出码由原命令决定,命令失败始终保留失败。结果包缺失或摘要不可读时显著标注 evidence incomplete(证据不完整)、计数未知,不改变命令结果,也不把缺失计数写成零。构建关不读取测试计数;未执行的关卡不等同于测试跳过。

统一入口成功表示所选关卡的命令通过,仍需核对证据是否完整;hostless、真实进程 smoke、原生 UI 和真实系统代理的验收边界仍分别成立,发布打包门槛见签名与分发。

task test 与 task test:smoke 显式启用 Xcode 单测试超时:默认 120 秒,上限 300 秒。这是 runner 兜底,不保证测试任务正常清理,也不是性能门槛。规则阶段等待失败会抛错退出测试体;受保护的测试任务先释放夹具,再取消并等待结束。规则延迟 gate 和同步阻塞夹具的默认等待上限为 10 秒,超时必须报告失败。共享 AsyncGate 保留由用例控制的取消语义。Swift Testing 的 detached worker 超时报告保留创建用例名称和源码位置;应同时核对命令退出码与测试计数,不能仅凭单例通过数判断成功。

常用任务

在工程目录内,日常格式化、静态检查、工程生成、构建和测试优先使用 Taskfile.dist.yml 定义的任务:

task format
task format:check  # 只检查格式,不改写源文件
task lint
task lint:fix  # 需要自动修复 SwiftLint 违规时使用;执行后再运行 task lint
task gen:prj   # 生成 Xcode 工程
task build     # 构建 Debug 版本 app
task test      # 运行单元测试(不含真实进程冒烟)
task test:smoke  # 运行真实进程冒烟测试(拉起真实 sslocal,见签名与分发文档的发布门槛)

task format 会格式化 App/、Tests/、Domain/ 和 Agent/ 下的 Swift 文件;task format:check 使用同一配置严格检查且不修改文件。task lint:fix 会修改源文件,执行后检查并复核 diff。

首次准备与测试范围

新 clone 或清单变更后,在工程目录内先取外部二进制,再生成工程(顺序不可颠倒):

Scripts/fetch-external-binaries.sh
task gen:prj

Swift Package Manager 从 project.yml 固定的正式版本解析 AgeSwift,并校验其发布 XCFramework。首次解析需要网络,消费者无需安装 Rust。Vendor/AgeSwift/LICENSE-MIT 来自固定版本,随应用打包到 Resources/ThirdParty/AgeSwift/。加密存储互操作测试还需要 brew install age 和可访问的登录钥匙串;测试使用 UUID 隔离的 service 并清理,不修改真实应用配置。命令行调试方法见加密格式文档。

无证书也能构建与跑单测:Debug 基线是 ad-hoc,不需要 Configs/Signing.local.xcconfig。只有要做 Developer ID 签名(发布,或让本机 Debug 产物过打包门槛)时才写该文件,内容见签名与运行形态基线。

Tests/Smoke/ 是真实进程冒烟(经 wrapper 拉起真实 sslocal 验证启动、握手、 ACL 路由与插件监管),不在默认 task test 内,由独立 scheme 按需运行。

编写与优化测试

Hostless 与打包资源

单测不以 app 为宿主(ADR-0021)。两条约定:

  • 测试需要 app 产物(嵌入二进制、打包面 Info.plist 断言)时,一律经 Tests/Support/AppArtifact.swift 定位。
  • 生产代码读取 app bundle 内打包资源(规则快照、LaunchDaemon 清单等)时,走注入缝——控制器的 appBundle 参数(生产默认 Bundle.main),测试夹具注入 AppArtifact.bundle。

背景与三处资源缝的由来见 ADR-0021。

测试框架与隔离

  • 新增测试默认用 Swift Testing(import Testing);UI 测试与性能测试依赖 XCTest 专属 API(XCUIApplication、measure 计时),继续用 XCTest;存量 XCTest 用例不回迁。
  • 单测多 runner 并行执行:新夹具只占用进程私有资源——运行时分配端口、临时目录、UUID 后缀命名空间,不写共享路径与固定名。

夹具与执行成本

  • 输入规模由断言目标决定。启停、保存、部署、回滚、状态投影等行为集成测试使用小型确定性输入,保留真实的被测工作流与编译路径;规则行为夹具优先复用 ProxyRuntimeFixture.controlFlowRuleSnapshot。
  • 验证打包资源、完整规则语义、大数据边界或性能的专用用例,显式注入完整数据。行为测试通过规则数据注入缝缩小输入;仍需清单等打包资源时保留 AppArtifact.bundle。
  • 小夹具保留场景依赖:覆盖、遮蔽、禁用、孤立记录等关系按用例构造;公共夹具中无关的域名避开用例自身的规则,避免意外吸收或重复使预期部署变成无操作。替身实现被测流程依赖的注册、退出、回执等生命周期语义。
  • 异步穿插优先用 expectation、gate 或可控时钟观测阶段;与断言无关的生产延迟通过注入缩短。保留用例刻意验证的超时、健康窗、进程监督等待与数据边界。
  • 存量 XCTest 按行为拆为可独立调度的具体测试类;共享夹具基类只放状态和工厂,不含 test* 方法,避免子类重复执行继承用例。拆类或调整夹具后核对用例清单,确认无遗漏、重复或新增跳过。
  • 优化耗时先记录同一配置下的全量墙钟、最慢类和用例,再分段测量定位计算与等待。优先消除测试目标之外的重复计算,再评估拆类并行或独立的生产性能优化;用例耗时合计除以墙钟表示执行重叠,不能当作 CPU 使用率。

代码风格工具

  • swift format 随 Xcode/Swift 工具链提供,无需单独安装(基线:Apple Swift 6.4);SwiftLint 需 brew install swiftlint(基线 0.65.0),钩子管理器需 brew install pre-commit。缺少必需工具时,暂存了 NG2 Swift 文件的提交会被拒绝。
  • 配置文件:.swift-format 是 Swift 6.4 工具链默认规则的快照(固定成文件,工具链升级不漂移);.swiftlint.yml 默认规则起步,只声明排除项与个别和 format 基线冲突的关闭项(文件内有触发案例注释)。
  • 仓库级 hook 配置位于 .pre-commit-config.yaml;安装方式和检查范围见下节「开发钩子」。
  • 调整规则只针对实际痛点:修改 .swift-format / .swiftlint.yml 的 PR 须给出触发案例,并同步更新本节中的基线版本。

开发钩子

新 clone 后,在仓库根目录安装 hooks:

brew install pre-commit  # 已安装时跳过
pre-commit install

如果之前启用过旧版 .githooks,先清除本地 Git 设置,再安装新 hook:

git config --local --unset core.hooksPath
pre-commit install

提交中包含 ShadowsocksX-NG2/ 下的 .swift 文件时才会运行这些 hook,其他文件跳过。swift format 会在工作区格式化暂存的文件;如果文件发生变化,pre-commit 会拦下本次提交。检查并暂存格式化结果后再提交。随后只读的 task format:check 会检查全部 NG2 Swift 源码目录,也会覆盖 formatter 无法自动修复的 lint-only 规则。SwiftLint 检查暂存文件;error 级违规拒绝提交,warning 不阻止提交。没有暂存 NG2 Swift 文件时这些 hook 会跳过;暂存了 NG2 Swift 文件但缺少必需工具时,对应 hook 会失败。工具安装与版本见上节「代码风格工具」。

领域文档语言

本项目的 术语表 和新建 架构决策记录(ADR) 使用简体中文,代码标识符与必要的英文术语保留原文;已有 ADR 保留原文,不作语言迁移。

AI 生成大量文档后,人与机器之间的沟通成为协作瓶颈:人的阅读、理解和审阅速度决定了文档能否有效推动开发。使用简体中文可以降低维护者的认知负荷,提高沟通与审阅效率。