惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
博客园 - 【当耐特】
Cloudbric
Cloudbric
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Attack and Defense Labs
Attack and Defense Labs
爱范儿
爱范儿
The Cloudflare Blog
腾讯CDC
Security Archives - TechRepublic
Security Archives - TechRepublic
TaoSecurity Blog
TaoSecurity Blog
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
C
Check Point Blog
Schneier on Security
Schneier on Security
S
Schneier on Security
J
Java Code Geeks
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
About on SuperTechFans
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
A
Arctic Wolf
S
Secure Thoughts
P
Palo Alto Networks Blog
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 三生石上(FineUI控件)
D
Darknet – Hacking Tools, Hacker News & Cyber Security
量子位
U
Unit 42
I
InfoQ
D
DataBreaches.Net
P
Privacy International News Feed
T
Troy Hunt's Blog
博客园 - 叶小钗
T
Threatpost
博客园 - Franky
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
IT之家
IT之家
www.infosecurity-magazine.com
www.infosecurity-magazine.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cisco Blogs

阿尔的代码屋 | 全栈技术笔记

在 Android Termux 环境下安装 Hermes Agent 的踩坑与完美解决实践 开发日志| 阿尔的代码屋 VS Code 连接 WSL 精确每 10 分钟掉线 排坑笔记 | 阿尔的代码屋 Android 模拟器代理联网与 No Internet WiFi 锁死排坑笔记 | 阿尔的代码屋 [object Object] typing_extensions 有用(四):使用 TypeIs 替代危险的 cast,做最严谨的类型收窄 | 阿尔的代码屋 GoRouter 结合 Isar 运行 Widget 测试并发/粘性线程死锁卡死排坑笔记 | 阿尔的代码屋 typing_extensions 有用(三):使用 Unpack 结合 TypedDict 给 **kwargs 装上透视眼 | 阿尔的代码屋 typing_extensions 有用(二):使用 @override 打造重构代码时的“防呆神器” | 阿尔的代码屋 Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋 typing_extensions 有用(一):使用 Self 终结继承时的类型推断灾难 | 阿尔的代码屋 基于 Cloudflare Pages 的纯前端 WebAssembly 应用自动化部署实践 | 开发日志 | 阿尔的代码屋 基于 VS Code 远程开发的 GPU Docker 容器自动清理方案实践 开发日志| 阿尔的代码屋 Flutter E2E 测试从 integration_test 迁移到 Patrol - 实践笔记 | 阿尔的代码屋 Linux/macOS 下 micromamba 报错 Shard Index not available 与极度卡顿 排坑笔记 | 阿尔的代码屋 critical libmamba Shell not initialized micromamba报错 subprocess 无法修改父 Shell - 排坑笔记 | 阿尔的代码屋 VS Code Remote-SSH: 彻底告别重复输入密码 - 排坑笔记 | 阿尔的代码屋 Git 报警: inexact rename detection was skipped - 排坑笔记 | 阿尔的代码屋 解决群晖 NAS SSL 证书自动部署难题 | 阿尔的代码屋 Windows 下 Git error: fatal: refusing to work with credential missing host field - 排坑笔记 | 阿尔的代码屋 VS Code 调试 Python 第三方库源码的配置 - 开发技巧| 阿尔的代码屋 VS Code Remote Tunnels本地连接调试Kaggle GPU - 开发技巧| 阿尔的代码屋 Windows下Git error: open(xxxx) Filename too long - 排坑笔记 | 阿尔的代码屋 记录2026年零预约港卡开户实录避坑 告别孤岛:拥抱 MCP 协议,为大模型打造标准“USB 接口” - 大模型实战 08 完结篇 | 阿尔的代码屋 从 ReAct 到 Workflow:基于云端 API 构建事件驱动的智能体 - 大模型实战 07 额外篇 | 阿尔的代码屋 基于 LlamaIndex ReAct 框架手搓全自动博客监控 Agent - 大模型实战 07 | 阿尔的代码屋 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 大模型微调实战详解 - 大模型实战 05 | 阿尔的代码屋 基于 ChromaDB 打造工程级 RAG 系统 - 大模型实战 04 | 阿尔的代码屋 拆解 Transformers 原理与图解 - 大模型实战 03 | 阿尔的代码屋 云端炼丹房 2:Kaggle 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 云端炼丹房:Google Colab 免费 GPU 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 Open WebUI 部署与 RAG 实战 - 大模型实战 02 | 阿尔的代码屋
Patrol iOS 集成测试排坑实录 - xcodebuild exit code 70 全链路解决 | 阿尔的代码屋
Algieba · 2026-05-31 · via 阿尔的代码屋 | 全栈技术笔记

核心摘要 (TL;DR)

  • 背景:Patrol E2E 测试在 Android 模拟器上全部通过后,需要在 iOS 模拟器上复现同样的测试覆盖。
  • 核心问题xcodebuild exited with code 70,测试总数始终为 0(Total: 0),模拟器被克隆多份。
  • 根因链条RunnerTests(Unit Test Bundle)→ 需改为 RunnerUITests(UI Testing Bundle)→ Patrol CLI 硬编码 -destination-timeout 1 导致设备匹配失败 → SUPPORTED_PLATFORMS 缺少 iphonesimulator → scheme 中 parallelizable="YES" 导致模拟器克隆 → pressBack() 在 iOS 上不可用。
  • 关键解法:重命名 target 为 RunnerUITests、修补 patrol_cli 源码用 device.id 替换 name+OS 匹配、禁用测试并行化、用 $.tester.pageBack() 替代 pressBack()

问题概览卡片

基本信息

  • 项目背景:Flutter 项目使用 Riverpod + GoRouter + Isar,已在 Android 上完成 E2E 测试。
  • 技术栈:Flutter 3.41.1, Patrol 3.20.0, patrol_cli 3.11.0, Xcode 16.x
  • 测试目标:将 Dashboard、Inventory、Settings、History 四大模块测试扩展到 iOS 模拟器
  • 运行环境:iOS 模拟器 (iPhone 16e)

错误日志复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 问题 1: xcodebuild 退出码 70,测试数量为 0
Test summary:
📝 Total: 0
✅ Successful: 0
✗ Failed to execute tests (xcodebuild exited with code 70)

# 问题 2: 无法找到模拟器设备
xcodebuild: error: Unable to find a device matching the provided destination specifier
Supported platforms for the buildables in the current scheme is empty.

# 问题 3: dylib 加载失败
Library not loaded: CocoaAsyncSocket.framework/CocoaAsyncSocket

# 问题 4: pressBack 在 iOS 上不可用
PatrolActionException: pressBack() failed with Invalid response: 400
method pressBack() is not implemented on iOS

# 问题 5: 模拟器被克隆
Test case started on 'Clone 1 of iPhone 16e - RunnerUITests-Runner'

1. 现象描述与现场还原

从 Android 到 iOS

Android E2E 测试全部通过后,执行 patrol test -d "iPhone 16e" 启动 iOS 测试。预期:像 Android 一样直接跑起来。实际:xcodebuild exited with code 70Total: 0,测试完全没有被发现。

排查过程时间线

整个调试过程跨越了多个问题层级,每修复一个问题就暴露下一个:

  1. Unit Test vs UI Test — 测试 target 类型不对
  2. 目标命名 — Patrol CLI 硬编码期望 RunnerUITests,不是 RunnerTests
  3. 框架嵌入inherit! :search_paths 不够,需要 :complete
  4. 设备匹配-destination-timeout 1 太短 + SUPPORTED_PLATFORMS 不全
  5. 模拟器克隆parallelizable="YES" 导致克隆
  6. 返回导航pressBack() 在 iOS 上不存在

2. 根本原因分析

问题 1: RunnerTests 是 Unit Test Bundle,不是 UI Testing Bundle

Flutter 默认创建的 RunnerTests target 的 productTypecom.apple.product-type.bundle.unit-test。Patrol 需要的是 com.apple.product-type.bundle.ui-testing

差异对比:

设置Unit Test BundleUI Testing Bundle (Patrol)
productType.bundle.unit-test.bundle.ui-testing
TEST_HOST需要设置不需要
BUNDLE_LOADER需要设置不需要
TEST_TARGET_NAME不需要需要 = Runner
Podfile 继承inherit! :search_pathsinherit! :complete

修改 project.pbxproj

1
2
3
4
5
// 修改前
productType = "com.apple.product-type.bundle.unit-test";

// 修改后
productType = "com.apple.product-type.bundle.ui-testing";

同时删除 BUNDLE_LOADERTEST_HOST,添加 TEST_TARGET_NAME = Runner

问题 2: Patrol CLI 期望 RunnerUITests,而非 RunnerTests

深入 Patrol CLI 源码 (app_options.dart) 发现:

1
2

...['-only-testing', 'RunnerUITests/RunnerUITests'],

无论你的 Xcode 项目中 target 叫什么名字,Patrol CLI 都会固定查找 RunnerUITests

解决方案: 全面重命名:

  • 目录:ios/RunnerTests/ios/RunnerUITests/
  • 文件:RunnerTests.swiftRunnerUITests.m(改用 Objective-C + Patrol 宏)
  • project.pbxproj 中所有引用
  • Podfile 中 target 名称

创建 Objective-C 测试入口:

1
2
3
4
5
6
// ios/RunnerUITests/RunnerUITests.m
@import XCTest;
@import patrol;
@import ObjectiveC.runtime;

PATROL_INTEGRATION_TEST_IOS_RUNNER(RunnerUITests)

问题 3: CocoaAsyncSocket 框架加载失败

1
Library not loaded: CocoaAsyncSocket.framework/CocoaAsyncSocket

UI Testing Bundle 运行在独立进程中,需要完整嵌入所有依赖框架。

修改 Podfile:

1
2
3
4
5
6
7
8
9

target 'RunnerUITests' do
inherit! :search_paths
end


target 'RunnerUITests' do
inherit! :complete
end

问题 4: 设备匹配失败 — destination-timeout 与 SUPPORTED_PLATFORMS

即使 target 配置正确,xcodebuild test-without-building 仍然报:

1
2
Supported platforms for the buildables in the current scheme is empty.
Unable to find a device matching the provided destination specifier

两层原因:

4a. SUPPORTED_PLATFORMS 不完整

project.pbxproj 中项目级别的 Release/Profile 配置:

1
2
3
4
5
6
// 修改前(只有真机)
SUPPORTED_PLATFORMS = iphoneos;

// 修改后(加上模拟器)
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;

4b. Patrol CLI 硬编码 -destination-timeout 1

patrol_cli 构造的 xcodebuild 命令中 -destination-timeout 只有 1 秒。当 SUPPORTED_PLATFORMS 不完整时,xcodebuild 需要更长时间解析设备。

但更根本的问题是 Patrol CLI 使用 -destination platform=iOS Simulator,OS=latest,name=iPhone 16e 按名称匹配设备,在 “Supported platforms is empty” 场景下完全无法工作。

解决方案:修补本地 patrol_cli 源码,使用设备 UUID 直接匹配:

1
2
3
4
5
6
7
8
9



...['-destination', 'platform=${...},name=${device.name}'],
...['-destination-timeout', '1'],


...['-destination', 'id=${device.id}'],
...['-destination-timeout', '30'],

注意: 修改源码后需要删除 snapshot 并重新激活:

1
2
rm ~/.pub-cache/global_packages/patrol_cli/bin/main.dart-3.11.0.snapshot
dart pub global activate patrol_cli 3.11.0

问题 5: 模拟器被克隆

测试运行时,xcodebuild 为每个 test suite 创建了独立的模拟器克隆(“Clone 1 of iPhone 16e”),导致启动多个模拟器实例。

原因: Runner.xcschemeTestableReferenceparallelizable 属性:

1
2
3
4
5

<TestableReference skipped="NO" parallelizable="YES">


<TestableReference skipped="NO" parallelizable="NO">

问题 6: pressBack() 在 iOS 上不可用

Android 测试中广泛使用的 $.native.pressBack() 在 iOS 上返回 400 错误:

1
2
pressBack() failed with Invalid response: 400
method pressBack() is not implemented on iOS

iOS 没有系统级返回按钮,这是 Android 独有的功能。

解决方案: 创建跨平台 goBack() helper:

1
2
3
4
5
6
7
8
9
Future<void> goBack(PatrolIntegrationTester $) async {
if (Platform.isAndroid) {
await $.native.pressBack();
} else {

await $.tester.pageBack();
}
await $.pumpAndSettle();
}

踩坑细节: iOS 上 Flutter AppBar 的返回图标是 Icons.arrow_back_ios_new_rounded(不是 arrow_back 也不是 arrow_back_ios)。直接用 Patrol 的 $(Icons.xxx).tap() 找到图标后会报 “not hit-testable”,因为图标嵌套在 AppBar 的约束区域内。$.tester.pageBack() 是最可靠的方案。


3. 解决方案

步骤一:修改 Xcode 项目 target 配置

project.pbxproj 中:

  1. productTypeunit-test 改为 ui-testing
  2. 删除 BUNDLE_LOADERTEST_HOST
  3. 添加 TEST_TARGET_NAME = Runner
  4. 全局重命名 RunnerTestsRunnerUITests
  5. 添加 SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"

步骤二:创建 Patrol iOS 测试入口

1
2
3
4
5
6
// ios/RunnerUITests/RunnerUITests.m
@import XCTest;
@import patrol;
@import ObjectiveC.runtime;

PATROL_INTEGRATION_TEST_IOS_RUNNER(RunnerUITests)

步骤三:更新 Podfile

1
2
3
4
5
6
7
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerUITests' do
inherit! :complete
end
end

步骤四:修复 Runner.xcscheme

1
2
3
4
5
6
7
8
9
10
11
12

<BuildActionEntry buildForTesting="YES" buildForRunning="NO" ...>
<BuildableReference
BlueprintIdentifier="331C8080294A63A400263BE5"
BuildableName="RunnerUITests.xctest"
BlueprintName="RunnerUITests"
ReferencedContainer="container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>


<TestableReference skipped="NO" parallelizable="NO">

步骤五:修补 patrol_cli 本地源码

1
2
3
4


...['-destination', 'id=${device.id}'],
...['-destination-timeout', '30'],
1
2
3

rm ~/.pub-cache/global_packages/patrol_cli/bin/main.dart-3.11.0.snapshot
dart pub global activate patrol_cli 3.11.0

步骤六:跨平台返回导航 helper

1
2
3
4
5
6
7
8
9
10
11

import 'dart:io' show Platform;

Future<void> goBack(PatrolIntegrationTester $) async {
if (Platform.isAndroid) {
await $.native.pressBack();
} else {
await $.tester.pageBack();
}
await $.pumpAndSettle();
}

所有测试文件中的 $.native.pressBack() 替换为 goBack($)

步骤七:更新 Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.PHONY: ios-boot
ios-boot:
xcrun simctl shutdown all || true
sleep 3
xcrun simctl boot "iPhone 16e"
open -a Simulator
sleep 10

.PHONY: e2e-ios
e2e-ios: ios-boot
$(PATROL) test -d "iPhone 16e" -t integration_test/modules/dashboard_test.dart
$(PATROL) test -d "iPhone 16e" -t integration_test/modules/inventory_test.dart
$(PATROL) test -d "iPhone 16e" -t integration_test/modules/settings_test.dart
$(PATROL) test -d "iPhone 16e" -t integration_test/modules/history_test.dart

4. 预防与建议

  • Patrol iOS 的 target 必须叫 RunnerUITests:这是 patrol_cli 硬编码的名称,不是可配置的。新项目请一开始就用这个名字。
  • productType 必须是 ui-testing:Unit Test Bundle 和 UI Testing Bundle 是完全不同的东西,Patrol 需要后者。
  • inherit! :complete 而非 :search_paths:UI Testing Bundle 在独立进程运行,需要完整嵌入所有框架。
  • Patrol CLI 的 snapshot 机制:修改 patrol_cli 源码后,必须删除 .snapshot 文件并重新激活,否则修改不会生效。
  • pressBack() 是 Android 独有:iOS 测试中必须用 $.tester.pageBack() 或手动点击 UI 中的返回按钮。
  • 模拟器管理:测试前先 xcrun simctl shutdown all 确保只有一个模拟器运行,避免克隆。
  • AlertDialog 关闭:iOS 上不能用 goBack() 关闭 AlertDialog(没有系统返回键),必须点击对话框中的按钮。

5. 最终成果

iOS 测试结果

模块测试内容状态耗时
Dashboard启动、搜索、筛选、添加物品、ChoiceChip 切换、详情页✅ 通过44s
Inventory添加、验证、详情、消耗、删除、编辑、高级详情、连续添加✅ 通过63s
Settings数据管理、分类、位置、语言切换、数据统计、反馈对话框✅ 通过43s
History空状态、消耗后出现、补货流程、多物品历史✅ 通过45s

问题解决链路图

1
2
3
4
5
6
7
8
9
10
xcodebuild exit code 70 (Total: 0)
└─ productType = unit-test → 改为 ui-testing
└─ target 名 RunnerTests → 改为 RunnerUITests
└─ BUNDLE_LOADER/TEST_HOST → 删除,加 TEST_TARGET_NAME
└─ inherit! :search_paths → 改为 :complete
└─ "Supported platforms is empty" → 加 iphonesimulator 到 SUPPORTED_PLATFORMS
└─ destination-timeout 1 + name 匹配 → 改用 device.id + timeout 30
└─ parallelizable="YES" → 改为 "NO"(防克隆)
└─ pressBack() not implemented → 用 $.tester.pageBack()
└─ arrow_back_ios_new_rounded not hit-testable → 用 pageBack() 而非 icon tap

运行命令

1
2
3
4
5
make ios-boot        
make e2e-ios
make e2e-android
make e2e-dashboard
make e2e-settings