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

推荐订阅源

Project Zero
Project Zero
WordPress大学
WordPress大学
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Visual Studio Blog
爱范儿
爱范儿
P
Proofpoint News Feed
F
Fortinet All Blogs
雷峰网
雷峰网
小众软件
小众软件
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Secure Thoughts
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
S
Security @ Cisco Blogs
Help Net Security
Help Net Security
GbyAI
GbyAI
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
B
Blog
MongoDB | Blog
MongoDB | Blog
月光博客
月光博客
H
Heimdal Security Blog
Google Online Security Blog
Google Online Security Blog
S
Security Affairs
云风的 BLOG
云风的 BLOG
Engineering at Meta
Engineering at Meta
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
O
OpenAI News
H
Hacker News: Front Page
博客园 - 叶小钗
Last Week in AI
Last Week in AI
S
Schneier on Security
The Last Watchdog
The Last Watchdog
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
MyScale Blog
MyScale Blog
Recorded Future
Recorded Future
博客园 - 【当耐特】
V
Vulnerabilities – Threatpost
大猫的无限游戏
大猫的无限游戏
N
News | PayPal Newsroom
The Hacker News
The Hacker News
A
Arctic Wolf

BlogFinder

日常漫步 Vol.24 之漫步前山河 - 雅余 周报 #1-聊聊本周的收获 - Edwin's Blog 我的OpenCode必装插件与Skill Write Something 掌中之物未必在掌握之中 · CRIVU PiliNara,一个更顺手的 PiliPlus 分支 「NekoEcho」:做一个必有回响的猫娘主题博客 2026-05 书影音总结 简化博客主题 - 安迪 你要加油呐 我第一次发布 npm 包 拾花小记#45:中考前的二三事 – 小改学习志 黛西花园5月游 #18 枇杷又熟了的五月月报 一些奇奇怪怪的需求?word仿方正书版的几个小操作 - Xiobb's Blog 0419 御温泉之旅 修复了一些bug,网站基本上趋于稳定了 - 新锐博客 又回到四十年前 如何定义成功 迷鹿屋2026已重新上线 科技冰火两重天+一周回顾 ${title} 热度退了,我反而用得更深了-咕咚同学 我到底该不该换个域名? 随身WIFI折腾记 - 安迪 博客撰写体验提升——hexo pro插件 为什么不用相机把屏幕上的接关密码拍下来? 国清寺与天台山 – Ouroboros ★★★★☆《挽救计划》——久违的经济上行感 - Davidの3号基地 删除右键“打开方式”里多余选项 第三周刊_No.53|一切都会被支付两次 安卓APP通话记录与录音上传踩坑记录 - 子舒的博客 天量下跌 inBox 笔记 2.3.8,把工具栏交给了你-咕咚同学 我把小龙虾搬到了微信-咕咚同学 安好 - 响石潭 Compound Engineering Plugin:让每个工程单元都比上一个更容易 MOSS-TTS Family:开源高质量语音与声音生成模型家族深度解析 Crawl4AI:专为 LLM 设计的开源 Web 爬虫与数据抓取工具 Build Your Own X:从零实现你最喜欢的技术——程序员进阶的终极资源清单 Anthropic Skills:用文件夹教 Claude 专业技能的开源框架 1年的去月球(下) - 梅之夏 欢迎回来。 简单讲讲 ASN.1 与 OID DTV - 直播聚合客户端 5.22-5.27 – 不兴江 还没去过鸭川 – 不兴江 张晶晶同学三刷林志颖 关于我 – 不兴江 爱与嫉妒 – 不兴江 港股被持续做空 备案码花了四百块-咕咚同学 一句话生成封面:我给公众号做了4种风格的AI封面生成技能 「官」方認證 再谈费曼学习法 2026-05-28T00:34:11+08:00 2026-05-28T00:28:45+08:00 离谱的英语学习指南:基于AI的英语进阶系统方法论 iii:零集成架构的后端统一运行时 Claude Code Harness:让 Claude Code 工作有迹可循的工程化框架 Heretic:全自动移除大语言模型审查机制的开源工具 MarkItDown:微软开源的万能文档转 Markdown 利器 Harness:让 Claude Code 秒变多智能体协作工厂 这段时间尽折腾AI Agent了,确实极大地提高了效率 近期动态:两个新站点正式上线啦 误判解除!zhouayuan.com 腾讯安全申诉成功 - 周阿源|玩具设计・插画日常・生活随笔 Ralph:让 AI 编码工具自主循环跑完所有 PRD 任务的量产神器 全都违法 – 个人工作记录 关于zhouayuan.com被误判 “含违规信息” 的说明与申诉记录 - 周阿源|玩具设计・插画日常・生活随笔 小米 MiMo v2.5 Pro 白嫖 最大的人间清醒,兜里有钱,但是不花。 夜晚靓歌(12):于文文现场solo - 王志勇的Blog 今日插画:风扬起的倔强 - 周阿源|玩具设计・插画日常・生活随笔 回门习俗 独立网卡 - 忘记了回忆 500亿入股人工智能企业 从命令行到桌面智能体-咕咚同学 第一性原理读书笔记 行者微评论223-加班の守株待兔-博客|政治与时事-风雨行者 ZOZO开源物理接触求解器:GPU加速的可扩展仿真引擎 OpenStock:开源股票市场交易平台技术深度解析 MoneyPrinterTurbo:基于AI的全自动短视频生成工具深度解析 Claude-Mem:为 Claude Code 构建的持久化记忆压缩系统 Twenty:可代码化定制的企业级开源 CRM 平台技术深度解析 2026-05-26T22:59:17+08:00 企业级开源大模型部署平台 GPUStack 实战教程 1年的去月球(上) - 梅之夏 Sevalla - 静态网站托管服务 不用翻墙、不用注册、不用月费,普通人也能用上 Claude Code 装修灯具要注意⚠️ 黄梅天先锋 - 游子微博 公安备案顺利办结,站点备案全部完成 - 周阿源|玩具设计・插画日常・生活随笔 第三次兑换天猫超市卡了宗宗酱-三维狐少儿编程 Don't think, feel. - Rolen's Blog 人这一辈子,到底图个什么 博客迁移 - Edwin's Blog 情感赛道写作模板 再现本轮行情的典型特征 裁员与平常心-咕咚同学 别让“偷懒”,成为隐私泄露的破绽
Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋
Algieba · 2026-06-18 · via BlogFinder

核心摘要 (TL;DR)

  • 背景:Flutter 单元测试与 Widget 测试本地单线程跑完全通过,但在 CI 上开启高并发(--concurrency=8)且收集覆盖率时,有 6 个测试用例始终稳定失败。
  • 核心问题
    1. IsarError: Could not download IsarCore library 报错,伴随 LateInitializationError: Local 'isar' has not been initialized
    2. CategorySelectorLocationSelector 两个选择器组件在模拟选择与退出时,频繁在 pumpUntilGone 步骤超时失败。
  • 根因链条
    1. HTTP 拦截冲突:在 Widget 测试 (testWidgets) 中,TestWidgetsFlutterBinding 会拦截所有的 HTTP 请求并强制返回 400。Isar 在 initializeIsarCore(download: true) 时若本地无动态库,会尝试去 GitHub 下载,从而在 Widget 测试中必然被拦截报错。
    2. 多线程抢占:在并发 concurrency=8 下,若 Widget 测试先于纯 Unit 测试启动,就会触发上述下载失败;同时,用 runAsync 包裹 setUp 内部的初始化会静默吞掉底层异常,导致 late Isar isar 变量未赋值即进入测试,表现为 late 初始化报错。
    3. UI 命中与超时:CI 机器在并发调度下性能受限,默认的 5 秒超时容易被耗尽;且直接 tap Icons.radio_button_unchecked 图标的 hit-test 命中稳定性不如直接点击包装层 IconButton
  • 关键解法
    1. 移除 setUp / tearDown 中吞掉异常的 runAsync 包装,使异常直接暴露。
    2. scripts/run_tests.py 并发运行前,内置预初始化机制(Pre-initialization):先静默、单线程跑一次纯 Unit 测试以预下载 Isar 动态库至项目根目录。
    3. 将选择器测试的 tap 目标重构为 IconButton 并将 pumpUntilGone 超时延长至安全的 20 秒。

问题概览卡片

基本信息

  • 项目背景:基于 Flutter 的移动端应用,使用 Riverpod + Isar 数据库,包含 578 个测试用例。
  • 技术栈:Flutter 3.41.1, Isar 4.0.0-dev.14 (isar_community), Xcode 16.x
  • 测试目标:实现 CI/CD(GitHub Actions)全自动并发测试(concurrency=8)与测试覆盖率收集。
  • 运行环境:Ubuntu 22.04 LTS (CI Runner) / macOS (Local)

错误日志复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 问题 1: Isar Core 动态库下载失败
IsarError: Could not download IsarCore library:
Stack Trace:
package:isar_community/src/native/isar_core.dart 154:5 _downloadIsarCore

# 问题 2: 变量未初始化错误
LateInitializationError: Local 'isar' has not been initialized.
When the exception was thrown, this was the stack:
#0 LateError._throwLocalNotInitialized (dart:_internal-patch/internal_patch.dart:215:5)
#1 main.<anonymous closure>.<anonymous closure> (test/features/settings/conflict_resolution_screen_test.dart)

# 问题 3: Widget 测试 HTTP 拦截警告
Warning: At least one test in this suite creates an HttpClient. When running a test suite that uses
TestWidgetsFlutterBinding, all HTTP requests will return status code 400, and no network request
will actually be made.

# 问题 4: 选择器组件超时
Exception: Timed out waiting for finder to disappear: Found 1 widget with type "LocationSelector": [...]
#0 WidgetTesterExtensions.pumpUntilGone (test/helpers/widget_test_helper.dart:177:5)
#1 main.<anonymous closure>.<anonymous closure>.<anonymous closure> (test/features/inventory/location_selector_test.dart:77:9)

1. 现象描述与现场还原

本地通过,CI 必挂

在开发分支上,本地单线程执行 flutter test 时,所有用例均可绿过。然而一旦提交代码,GitHub Actions 上的测试流总会遭遇 6 个 Case 的顽固失败。测试汇总输出:

1
2
3
4
5
6
7
8
9
10
================================================================================
=========================== TEST RUN SUMMARY ===========================
================================================================================
Result: FAILED
Duration: 179.31 seconds
Passed: 572
Failed: 6
Skipped: 0
Total: 578
================================================================================

排查过程时间线

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

  1. 第一阶段:捕获异常源头
    原测试代码在 setUp 中使用了 TestWidgetsFlutterBinding.ensureInitialized().runAsync() 来初始化 Isar。这导致异常在 setUp 阶段被隐式吞掉,并在测试主体中以混淆的 LateInitializationError 形式呈现。通过移去该包装,让底层异常在第一秒暴露,露出了真实的 IsarError: Could not download IsarCore library:
  2. 第二阶段:揭示 Mock 拦截
    观察控制台警告,确认了由于 Widget 测试特殊的 TestWidgetsFlutterBinding 机制,任何在 testWidgets 内部由动态库发起的 HttpClient 网络下载请求,都会被自动导向 400 错误,直接导致了下载失败。
  3. 第三阶段:并发竞争与超时
    在本地开启 --concurrency=8 压测,复现了 CategorySelectorLocationSelector 在慢速 CI 下的超时表现。

2. 根本原因分析

问题 1:Widget 测试网络“天生被阉割”与 Isar 的动态下载

Isar 数据库在启动时需要加载底层的高性能原生动态库(如 macOS 的 libisar.dylib 或 Linux 的 libisar.so)。在测试辅助类中,代码设置了自动下载:

1
await Isar.initializeIsarCore(download: true);

但在 testWidgets 容器中,Flutter 为了保证 UI 测试的纯净和速度,会将真实的 HttpClient 替换为一个 Mock 客户端,默认对所有真实的网络访问返回 400 错误

如果 CI 并行运行时,第一个执行 Isar 初始化的线程碰巧是个 Widget 测试(或者两个测试线程同时写入动态库),就会因断网或死锁导致下载崩塌:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                  并发运行时的断网崩溃链路
──────────────────────

Test Suite (concurrency = 8)

┌──────────────┼──────────────┐
▼ ▼ ▼
[Unit Test] [Widget Test] [Widget Test]
│ │ │
│ │ (被 TestWidgetsFlutterBinding 限制)
│ │ │
▼ ▼ ▼
正常下载并写入 尝试请求网络 尝试请求网络
项目根目录 被强制阻断 被强制阻断
│ │ │
▼ ▼ ▼
Success! Blocked (400) Blocked (400)
│ │
▼ ▼
IsarError IsarError

问题 2:runAsync 捕获异常但“装作无事发生”

为什么之前没有看到 IsarError,而全是 LateInitializationError
因为原 setUp 块把初始化塞进了 runAsync

1
2
3
4
5
setUp(() async {
await TestWidgetsFlutterBinding.ensureInitialized().runAsync(() async {
isar = await IsarTestHelper.createTestIsar();
});
});

在 Flutter 绑定层中,runAsync 会接管 Zone 的未捕获异常并交由 FlutterError.onError 处理,但它不会主动向外层测试框架抛出异常中断 setup。这使得整个 setUp 误判为成功,而 late Isar isar 变量实际为 null。测试代码一运行直接触发 late 初始化未完成 the 报错。

问题 3:图标点击漂移与硬编码 5 秒超时

在分类和位置选择器的测试中,原本采用以下点击与等待方式:

1
2
3
4
5
6
final radioButton = find.descendant(
of: beverageTile,
matching: find.byIcon(Icons.radio_button_unchecked),
);
await tester.tap(radioButton);
await tester.pumpUntilGone(find.byType(CategorySelector));
  • 点击偏移Icons.radio_button_unchecked 是没有交互热区的纯 Icon 组件。测试框架计算其几何中心进行点击时,极易受到布局约束、边缘空白及多线程调度卡顿的影响,导致点击未落在真正的 IconButton 上,从而未触发返回而跳转了深层页面。
  • CI 超时限制:CI 机器在 8 并发下 CPU 极度饥饿,5 秒的时间窗口极易被卡顿消耗殆尽,导致 pumpUntilGone 抛出超时异常。

3. 解决方案

要解决这两个核心问题,我们需要建立预初始化(Pre-initialization)机制,并对组件选择器用例进行稳定性加固

步骤一:在运行器中注入“预初始化(Pre-init)”步骤

我们在并发测试脚本 [run_tests.py](file:///Users/mac/flutter/UseUp/scripts/run_tests.py) 启动并发测试之前,内置预初始化操作:通过单线程方式,静默且快速地跑一次纯单元测试。因为纯单元测试不受 TestWidgetsFlutterBinding 的 HTTP 约束,它可以通过真实网络把 Isar Core 动态库平稳地下载并保存到项目根目录下。

修改 [scripts/run_tests.py](file:///Users/mac/flutter/UseUp/scripts/run_tests.py):

1
2
3
4
5
6
7
8
9
10
11


print(f"{BOLD}{BLUE}📦 Pre-initializing Isar Core to avoid network/download conflicts...{RESET}")
pre_init_cmd = cmd_parts + ["test", "test/data/database_seeder_test.dart"]
try:
subprocess.run(pre_init_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception as e:
print(f"{YELLOW}Warning: Isar Core pre-initialization returned an error: {e}{RESET}")


print(f"{BOLD}{BLUE}🚀 Running: {' '.join(cmd)}{RESET}\n")

步骤二:移去吞噬异常的 runAsync 包裹

让 setup 逻辑保持简单直白,如果有 Isar 冲突能够直接抛出真实堆栈:

在 [conflict_resolution_screen_test.dart](file:///Users/mac/flutter/UseUp/test/features/settings/conflict_resolution_screen_test.dart) 及其他相关测试中:

1
2
3
4
5
6
7
8
9
10
11
12
13
     setUp(() async {
- await TestWidgetsFlutterBinding.ensureInitialized().runAsync(() async {
- isar = await IsarTestHelper.createTestIsar();
- });
+ isar = await IsarTestHelper.createTestIsar();
});

tearDown(() async {
- await TestWidgetsFlutterBinding.ensureInitialized().runAsync(() async {
- await isar.close();
- });
+ await isar.close();
});

步骤三:精准点击与增加超时窗口

我们将 tap 目标升级为具有独立手势响应热区的 IconButton 容器,同时将页面淡出等待超时延长至 20 秒,以应对慢速 CI Runner。

在 [category_selector_test.dart](file:///Users/mac/flutter/UseUp/test/features/inventory/category_selector_test.dart) 和 [location_selector_test.dart](file:///Users/mac/flutter/UseUp/test/features/inventory/location_selector_test.dart) 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
       await tester.runAsync(() async {
final beverageTile = find.ancestor(
of: find.text('Beverage'),
matching: find.byType(ListTile),
);
final radioButton = find.descendant(
of: beverageTile,
- matching: find.byIcon(Icons.radio_button_unchecked),
+ matching: find.byType(IconButton),
);
await tester.tap(radioButton);
- await tester.pumpUntilGone(find.byType(CategorySelector));
+ await tester.pumpUntilGone(
+ find.byType(CategorySelector),
+ timeout: const Duration(seconds: 20),
+ );
});

4. 预防与建议

  • Widget 测试的“断网”常识:时刻记住 testWidgets 内部是无法发起真实网络请求的。如果第三方插件或包需要在测试时在线拉取静态资源/原生库,务必提供离线 Stub 或是像本文一样,在测试集运行前将其拉取到本地。
  • 阻止 runAsync 接管初始化:除非是特定的异步等待逻辑,否则不要在 setUp / tearDown 块中滥用 runAsync。它会使得框架无法正确归集异常,掩盖真实的 setup 崩溃。
  • UI 测试中避免定位无热区的子 Widget:写 Widget 测试时,在做 tap 操作定位时,优先定位到 IconButtonTextButton 或有 GestureDetector 包裹的 Widget 实体,而不是它们内部的 IconText,因为这可能引起 hit-test 命中偏移。
  • 超时时间要留有余量:针对具有路由切换动画的 pumpUntilGone / pumpAndSettle 操作,不要硬编码太短的超时限制(如 5 秒),高并发测试时 CPU 满载会导致时间成倍拉长。

5. 最终成果

预加载与并发测试结果

更新运行器和测试用例后,本地与 CI 执行测试脚本的输出完全正常,顺利避开了所有的动态库下载冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
📦 Pre-initializing Isar Core to avoid network/download conflicts...
🚀 Running: flutter test --machine --concurrency=8

================================================================================
=============================== TEST RUN SUMMARY ===============================
================================================================================
Result: PASSED
Duration: 28.13 seconds
Passed: 578
Failed: 0
Skipped: 0
Total: 578
================================================================================

✨ All tests passed successfully!

全链路解决关系图

1
2
3
4
5
6
7
8
Isar 库下载冲突 & Widget 测试 400 异常
├─ TestWidgetsFlutterBinding 自动 Mock 并断网
│ └─ 解决:在 python 启动前静默串行跑 Unit 测试,把动态库预先下载到根目录
├─ setUp 里的 runAsync 静默吞掉异常,误报 late 初始化错误
│ └─ 解决:移除 runAsync,使底层错误直接断开 setup 抛出
└─ 选择器组件在并发 CPU 负载下频频超时
├─ 解决 1:将 tap 目标由 Icon 精确指向外层 IconButton 容器
└─ 解决 2:将 pumpUntilGone 的极限超时拉伸至 20 秒