测试
三个层面检验 MacroSIP:MacroSIPCore 的 Swift Testing 测试套件、针对 pjsua 的端到端环回通话,以及使用模拟引擎的应用演示启动。三者都在每次推送和拉取请求时在 CI 中运行。只有在 swift build 干净通过、测试通过、scripts/selftest-call.sh 通过并且验证了一次真实通话之后,一个里程碑才算完成(ADR 0006,docs/DEVELOPMENT.md)。
单元与引擎测试:scripts/test.sh
scripts/test.sh # 所有测试套件
scripts/test.sh --filter DialStringTests # 额外参数传给 swift test切勿直接运行 swift test:命令行工具附带 Testing 框架,但 SwiftPM 自己找不到它的宏插件。test.sh 显式传入插件路径(-Xswiftc -plugin-path -Xswiftc .../usr/lib/swift/host/plugins/testing)。没有 Xcode 就没有 XCTest,因此每个测试套件都使用 import Testing。
Tests/MacroSIPCoreTests 中的测试套件包含约 210 个测试:
| 文件 | 覆盖内容 |
|---|---|
DialStringTests |
解析表(sip:、tel:、普通号码、user@host) |
JSONStoreTests、StoreTests、SettingsCompatTests |
原子写入、损坏文件恢复、存储往返、旧版 settings.json 和通话记录文件以默认值解码、通话/定时勿扰/录音 |
KeychainTests |
使用测试服务名的封装 |
ContactTests |
号码规范化、匹配、排序、照片、旧字段 |
AutoAnswerPolicyTests |
规则顺序(勿扰、另一路通话、answer-after、模式) |
QuickKeyTests |
键模型、代接 URI、在线状态注册表 |
RecordingConverterTests |
WAV 转 M4A、立体声分离对、头部修复 |
EngineTests |
启动/停止、账户、环回通话(一个引擎呼叫自己)、保持、静音、DTMF、转接、TLS 监听器 |
EngineMilestone5Tests |
会议、单声道和立体声录音、针对 PIDF 存根的在线状态映射、answer-after 信任 |
EngineAudioTests |
MOS 表、每路通话和铃声的音量、Opus FEC/DTX、语音处理策略、设备事件去抖 |
EngineCallOptionsTests |
单通话模式、呼叫等待提示音、DTMF 方式、勿扰模式 |
EngineNetworkTests |
注册退避、网络回合、re-INVITE 结果、媒体看门狗、睡眠与唤醒,使用 NetworkStub(一个 UDP 注册服务器/被叫方) |
Engine262Tests |
RTP 端口窗口、rport、使用进程内 DNS 存根的 DNS SRV、拒接规则、附加编解码器 |
DirectoryParsingTests、DirectoryServiceTests、DirectoryLDAPTests |
CSV/JSON 源与号码匹配、使用存根提供者的服务、针对进程内 NWListener 服务器的 LDAP |
引擎测试使用空音频、绑定到环回地址,并使用 Engine(monitorNetwork: false),因此不依赖主机的网络;makeEngine() 会停止失败测试遗留的引擎,所以一次失败不会级联。TestSupport.swift 提供临时目录。
端到端通话:scripts/selftest-call.sh
scripts/selftest-call.sh # 原生
SELFTEST_ARCH=x86_64 scripts/selftest-call.sh # Rosetta 2 下的 x86_64 切片
SELFTEST_LOG=/tmp/selftest.log SELFTEST_LOG_LEVEL=5 scripts/selftest-call.sh该脚本从 third_party/pjsip/bin/pjsua 在端口 5080 和 5081 上启动两部 pjsua 电话(空音频、自动接听、循环的 600 Hz 音、5088 和 5089 上的 telnet CLI),构建 macrosip-cli 并运行 macrosip-cli --selftest。自测(Sources/macrosip-cli/SelfTest*.swift)针对一个独立的 PJSIP 实现检查:双向 RTP、保持与恢复(也包括连续操作)、DTMF、单声道录音(可听到音调的 WAV,然后是 M4A)、中间一秒静音的立体声分离录音、通过 pjsua CLI 驱动的在线状态(在线、离开、忙)、带合并和拆分的两路会议以及在两路之间移动的录音、来电(接听、以 486 拒绝)、两部电话之间的盲转接和协商转接、通过 SIP INFO 的 DTMF 以及针对 SIPStub(macrosip-cli 内部一个发出摘要认证挑战的 UDP 注册服务器;每个已授权的 INVITE 必须超过 1300 字节且没有传输切换错误)注册的账户、通话期间和已注册账户下的模拟网络变化、通话质量(必须为 good)、单通话模式、勿扰模式、拒接规则以及仅 GSM 的通话。使用 SELFTEST_ARCH=x86_64 时,CLI 以通用方式构建,CLI 和两部电话都通过 arch -x86_64 运行;pjsua 在 Rosetta 下需要多几秒才能就绪,因此脚本会轮询其 CLI 最多约 15 秒。SELFTEST_CLI=<path> 使用预构建的 CLI。
演示启动与模拟引擎
debug 应用(scripts/make-app.sh debug)接受 Sources/MacroSIPApp/Model/LaunchOptions.swift 中的启动选项;release 应用忽略它们。
| 选项 | 效果 |
|---|---|
--mock-engine |
用 MockEngine 代替 PJSIP:注册在 1 秒后成功(密码 wrong 或包含 fail 的域给出 403),外拨在 2 秒后接通 |
--demo-state <name> |
植入示例账户、联系人和记录条目,隐含 --mock-engine 和一个一次性数据目录;绝不触碰钥匙串 |
--data-dir <path> |
JSON 存储放在 <path> 而非 Application Support |
--snapshot <prefix> |
在 --snapshot-after 秒(默认 6)后为每个窗口写出 <prefix>-<window>.png,然后退出;无需显示器 |
--appearance light|dark |
强制主题 |
-AppleLanguages "(ru)" |
标准的 macOS 参数;以俄语运行应用 |
--log-level 0-6 |
PJSIP 详细程度(所有构建) |
按领域划分的演示状态(完整列表在 Model/DemoState.swift 的头部):
- 主窗口与通话:
first-launch、dialpad、contacts、calllog、connected、calls、conference、recording、recording-long、incoming、incoming-hidden、autoAnswer、suggest、keypad、transfer-banner、account-popup、account-popup-live、many-accounts。 - 音频与网络:
quality-good、quality-fair、quality-poor、quality-poor-calls、call-volume(同时写出<prefix>-popover.png)、headset、headset-back、network-offline、reconnecting。 - 通话与勿扰:
timed-dnd(将菜单栏项写入<prefix>-menu.txt)、call-log-playback、busy-mode。 - 设置:
settings-accounts、account-editor、account-editor-basic、account-editor-open、editor-first-launch、settings-general、settings-general-advanced、settings-audio、settings-codecs、settings-network、settings-recording、settings-autoanswer、settings-calls、settings-directory、many-accounts-settings、deny-incoming。 - 快捷键:
quick-keys、quick-keys-bottom(写出-main.png和-settings.png)。 - 联系人名片:
contact-card、contact-card-new(写出-card.png)。 - 目录(
MockDirectory):directory、directory-done、directory-incoming、directory-sheet、directory-sheet-url、directory-sheet-google、directory-sheet-macos、directory-import、directory-selftest。 - 消息(模拟引擎 0.6 秒后应答):
messages、messages-conversation、messages-failed、messages-new、messages-empty、messages-many、messages-selftest。每个状态都会在演示设置中开启消息;messages-selftest只运行selftest也包含的消息检查(关闭:丢弃、无标签页;开启:保存;再次关闭:隐藏)。 - 维护:
selftest、quit(检查关闭不会挂起)。
演示自测。--demo-state selftest 像视图那样通过模拟引擎驱动模型,并为每项检查记录一行 PASS 或 FAIL:注册状态(一个账户已注册,一个以 403 失败)、呼叫中和已接通状态、从联系人解析出的来电者姓名、静音、保持与恢复、两路通话的保持切换、协商转接、录音回放、账户测试、状态摘要等。结果输出到 stdout,使用 --snapshot 时输出到 <prefix>-selftest.txt,然后应用退出。--demo-state directory-selftest 只运行目录部分:号码栏中的建议、来电者查询、导入与合并。
timeout 60 dist/MacroSIP.app/Contents/MacOS/MacroSIP --demo-state selftest
timeout 60 dist/MacroSIP.app/Contents/MacOS/MacroSIP --demo-state directory-selftest字符串:scripts/check-strings.sh
检查 Model/Strings.swift 中使用的每个键都存在于 en.lproj 和 ru.lproj(Localizable.strings 或 Localizable.stringsdict)中,没有文件包含代码未使用的键,没有键同时出现在两个文件中,复数类别完整(英语为 one/other,俄语为 one/few/many/other),并且俄语值没有使用英语值所没有的占位符。每次更改字符串后都要运行它。
截图
docs/screenshots/{light,dark} 和 docs/screenshots/ru/{light,dark} 中的图像来自带 --snapshot 的演示启动;没有单独的截图脚本。俄语深色变体的典型截图:
timeout 30 dist/MacroSIP.app/Contents/MacOS/MacroSIP --demo-state contacts \
--appearance dark -AppleLanguages "(ru)" --snapshot /tmp/shots/contacts --snapshot-after 3有来电响铃或打开了表单的状态永不自行退出,因此需要 timeout。俄语文本大约长 25 %,所以添加字符串后请检查状态栏、通话面板按钮和账户弹出菜单。模拟中的 Google 通讯录在 4 秒后应答;用 --snapshot-after 3 捕捉“正在搜索…”行。
CI
.github/workflows/ci.yml 在对 main 的推送和拉取请求时运行,每个 ref 同时只有一个进行中的运行。
| 作业 | 运行器 | 步骤 |
|---|---|---|
build |
macos-15(arm64) |
恢复 third_party/pjsip 和 third_party/deps 的缓存(键:build-pjsip.sh 和 config_site.h 的哈希)、build-pjsip.sh、swift build、test.sh、原生以及在 Rosetta 2(按需安装)下以 SELFTEST_ARCH=x86_64 运行 selftest-call.sh、make-app.sh release、对二进制文件执行 lipo -info;将应用(14 天)和通用 PJSIP 构建(3 天)作为产物上传,失败时上传构建日志 |
intel |
macos-15-intel(x86_64),在 build 之后 |
下载通用库和应用(Intel 主机无法运行 build-pjsip.sh),验证签名,用 --data-dir 和 MACROSIP_LOG_DIR 启动应用 15 秒,然后原生运行 swift build、test.sh 和 selftest-call.sh |
release.yml 在构建 DMG 之前重复 arm64 的步骤(见 发布)。