云端 Mac CI 为 iOS UI 测试录制失败现场视频

CI/CD 实践 ·约 7 分钟阅读

云端 Mac CI 为 iOS UI 测试录制失败现场视频

UI 测试在云端 Mac 上失败时,单看断言和截图往往不够。弹窗出现后立即消失、页面转场停在中间、键盘遮住按钮,这些问题只有连续画面能说明发生顺序。可靠的做法不是永久保存所有录像,而是在测试启动前对指定模拟器录屏,测试成功就删除,失败才把视频、日志和结果包一起归档。

先固定测试设备与取证边界

不要直接把 booted 交给录屏命令。并发任务、上一次异常退出的设备或人工调试会让多台模拟器同时处于启动状态,录像可能来自错误设备。先创建或查找本任务专用设备,取得 UDID,再把它传给启动、测试、录屏和清理步骤。

DEVICE_NAME="CI-iPhone"
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-0"
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"

UDID="$(xcrun simctl list devices available -j |
  plutil -extract devices json -o - - |
  jq -r --arg name "$DEVICE_NAME" '
    .. | objects | select(.name? == $name) | .udid
  ' | head -n 1)"

if [ -z "$UDID" ]; then
  UDID="$(xcrun simctl create "$DEVICE_NAME" "$DEVICE_TYPE" "$RUNTIME")"
fi

xcrun simctl boot "$UDID" 2>/dev/null || true
xcrun simctl bootstatus "$UDID" -b

运行时标识随已安装的 Xcode 变化,流水线应在执行前通过 xcrun simctl list runtimes 验证,而不是假设每台机器都具备同一版本。多个任务并发时,设备名还应包含任务编号,避免共享状态。

录像的目标是补足失败时间线,不是替代日志。视频回答“界面先发生了什么”,日志和 xcresult 回答“测试框架记录了什么”。

让录屏进程可靠收尾

simctl io recordVideo 会持续运行,必须由脚本保存其 PID。测试结束后不要立刻发送强制终止信号;先发送 INT,让工具写完视频容器索引,再等待进程退出。

ARTIFACTS="${PWD}/artifacts"
VIDEO="${ARTIFACTS}/ui-test.mp4"
RESULT="${ARTIFACTS}/UITests.xcresult"
LOG="${ARTIFACTS}/xcodebuild.log"

mkdir -p "$ARTIFACTS"
rm -rf "$RESULT"
rm -f "$VIDEO" "$LOG"

xcrun simctl io "$UDID" recordVideo \
  --codec=h264 \
  --force "$VIDEO" &
RECORDER_PID=$!

finish_recording() {
  if kill -0 "$RECORDER_PID" 2>/dev/null; then
    kill -INT "$RECORDER_PID"
    wait "$RECORDER_PID" || true
  fi
}

trap finish_recording EXIT INT TERM

H.264 通常便于在浏览器和工单附件中预览。若团队后续需要逐帧分析,可在确认产物系统兼容后改用其他编码,但不要在同一流水线中混用格式,否则查看工具和保存策略会变得不可预测。

保留真实的测试退出码

录屏是旁路进程,不能掩盖 xcodebuild 的结果。最稳妥的方式是暂时关闭遇错退出,单独保存测试退出码,再正常结束录像。若还要通过 tee 显示日志,必须读取管道中 xcodebuild 的退出状态,不能误用 tee 的成功状态。

set +e
xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "platform=iOS Simulator,id=${UDID}" \
  -resultBundlePath "$RESULT" \
  >"$LOG" 2>&1
TEST_STATUS=$?
set -e

finish_recording
trap - EXIT INT TERM

if [ "$TEST_STATUS" -eq 0 ]; then
  rm -f "$VIDEO"
  exit 0
fi

exit "$TEST_STATUS"

这里刻意不在测试失败后立即退出,因为录像仍需完成收尾。resultBundlePath 使用每次任务独立的路径,执行前删除旧目录,否则旧结果可能与本次失败混在一起。

验证视频而不是只看文件存在

零字节文件、只有容器头的短文件以及被强制中断的录像都不具备诊断价值。归档前至少检查文件大小,并用系统媒体工具读取时长。测试刚启动就失败时,视频很短是合理现象,因此时长阈值不宜设置得过高。

检查项 建议动作 失败时处理
文件存在 test -f 保留日志并标记录像缺失
文件非空 test -s 检查录屏 PID 与磁盘空间
可读取时长 查询媒体元数据 视为损坏文件,不上传空壳
设备一致 比对记录的 UDID 终止任务并清理错误设备
测试结果包 检查 xcresult 目录 保留原始日志作为兜底

可把 UDID、测试开始时间和录像路径写入普通文本清单,与视频一起归档。不要把访问凭据、环境变量全集或未脱敏命令行写入清单。视频本身也可能出现通知、测试账号名称或业务数据,测试夹具应使用专门的非敏感样本。

处理并发、超时与残留进程

每个任务应拥有独立的模拟器、结果目录和录像文件名。共享 booted、固定的 artifacts/ui-test.mp4 或公共 DerivedData 目录,都会让并发任务互相覆盖。任务级目录可使用流水线提供的唯一编号生成,并在退出时删除成功任务的临时文件。

外层超时器终止测试时,清理函数仍要获得执行机会。先向测试进程发送可处理的终止信号,预留数秒让 trap 完成录像收尾,最后才执行强制清理。下一次任务开始前,可检查是否存在父进程已经退出的 simctl io 录屏进程,但不要用宽泛的 killall,否则会误伤同机其他任务。

稳定的归档集合通常只有三项:失败视频、xcresult 结果包和原始构建日志。三者使用同一任务编号命名,保留周期也应一致。这样从失败列表进入某次任务后,可以先看视频确定界面状态,再从结果包定位断言和附件,最后用日志核对命令、目标设备与退出码,形成完整而不过量的取证链。

常见问题

为什么不能直接对 booted 模拟器录屏?

并发任务可能同时启动多个模拟器,booted 无法保证指向当前测试设备。应先取得目标模拟器 UDID,再让启动、测试、录屏和清理都使用同一个 UDID。

为什么录屏进程结束后生成的视频可能无法播放?

直接强制终止可能来不及写完视频容器索引。应向录屏进程发送 INT 信号并等待其退出,再检查文件大小和媒体元数据。

成功的测试也需要保存录屏吗?

通常不需要。成功任务删除视频,只在失败时连同 xcresult 和测试日志归档,可以控制存储量并缩短产物上传时间。

Debug M4

在独享 Apple Silicon 物理节点上执行下一次构建

选择租期与节点区域,核对 Mac Mini M4、16GB RAM、256GB SSD 和应付 USD 金额后提交订单。

立即租用云端 Mac