首页/新闻资讯/正文详情

Hypothesis 多 Bug 发现(Multi-Bug Discovery):一次运行同时报告所有不同故障的机制与原理

发布时间:2026/9/25 10:48:36 来源:云帆数科 栏目:资讯中心
Hypothesis 多 Bug 发现(Multi-Bug Discovery):一次运行同时报告所有不同故障的机制与原理
测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载Hypothesis 作为 Python 生态中主流的基于属性的测试property-based testing库其核心能力之一是在找到触发 bug 的示例后自动将其收缩shrink为最小复现。从 Hypothesis 3.29.0 起Hypothesis 又引入了一项重要能力——多 Bug 发现multi-bug discovery一次测试运行中同时定位、独立收缩并完整报告多个不同故障而不是让一个简单的 bug 掩盖其余更复杂的 bug。本文以官方技术博客《When multiple bugs attack》为骨架结合当前仓库源码深入讲解这一特性的动机、行为、判定启发式、实现机制与配置方法。一、问题的起点收缩过程中的 Bug 滑移Bug Slippage属性测试的基本流程是生成随机输入 → 运行测试 → 发现失败 → 收缩失败用例。绝大多数属性测试库以及独立的测试用例缩减器如用于外部项目 bug 报告的工具都具备收缩能力——把触发 bug 的巨大输入逐步化简成几行就能复现的最小用例。但这里存在一个被普遍忽视的问题你怎么知道收缩前后触发的是同一个 bug这并非学术思辨。收缩过程中从原来那个 bug 滑到另一个 bugbug slippage是非常常见的现象。文档中给出了一个经典例子from hypothesis import given, strategies as st def mean(ls): return sum(ls) / len(ls) given(st.lists(st.floats())) def test(ls): assert min(ls) mean(ls) max(ls)这个测试存在多种完全不同的失败方式传入NaN均值与min/max的比较关系被破坏传入[-float(inf), float(inf)]正负无穷引发数值问题传入会触发浮点精度误差的数字组合。但经过收缩之后我们得到的往往是空列表——测试因min()作用于空序列而抛出ValueError。也就是说一个可能非常有趣、稀有的数值 bug被一个平庸且常见的错误对空序列取最小值所掩盖。滑移的两面性文档明确指出滑移并非全然坏事正面通过滑移可能发现更多 bug其中一些甚至是 Hypothesis 原本注意不到的负面在多数场景下一个有趣而稀有的 bug 滑移成一个无聊而常见的 bug会显著降低报告的诊断价值也打乱你修复 bug 的优先级。二、历史方案示例数据库的回放补偿在引入多 Bug 报告之前Hypothesis 已经比多数同类库做得更好——它依赖Hypothesis 示例数据库example database。由于所有中间出现的失败用例都会被存入数据库当你重新运行测试时其中一部分会被回放。因此如果你修好了当前暴露的那个 bug 再重跑测试此前被那个更简单 bug 隐藏的其他故障就会浮现出来。这套机制有效但用户体验并不理想你获得的信息量远低于理论上限而且修复顺序由 Hypothesis 的发现顺序决定而不是由你按价值排序。更理想的做法是让 Hypothesis 把找到的所有 bug 一次性告诉你由你自己决定先修哪个。从 Hypothesis 3.29.0发布于 2017 年开始这一目标得以实现。三、新行为一次运行完整报告所有故障以本文第一节的mean测试为例在新版本下运行会同时输出两组Failing test case并最终抛出一个聚合异常Failing test case: test(ls[nan]) Traceback (most recent call last): ... File broken.py, line 9, in test assert min(ls) mean(ls) max(ls) AssertionError Failing test case: test(ls[]) Traceback (most recent call last): ... File broken.py, line 9, in test assert min(ls) mean(ls) max(ls) ValueError: min() arg is an empty sequence You can add seed(67388524433957857561882369659879357765) to this test to reproduce this failure. Traceback (most recent call last): ... hypothesis.errors.MultipleFailures: Hypothesis found 2 distinct failures.注文档作者也坦言当时的堆栈信息比较冗余并为此开过清理工单。这段输出包含了几个关键信息每个不同故障都有独立的收缩后最小用例与其完整堆栈测试以MultipleFailures: Hypothesis found 2 distinct failures收尾同时给出可用于复现的seed种子值。所有 bug 都被同时独立收缩文档特别强调所有不同 bug 的用例是同步最小化的并且每个用例都完整利用 Hypothesis 的收缩能力。也就是说每一个 bug 的报告可读性都与假设只发现了这一个 bug时一样好。你不会因为同时报告多个 bug 而得到缩水的最小用例。四、判重启发式异常类型 抛出位置多 Bug 发现的核心问题是如何判定两个示例属于同一个 bug。文档明确描述了 Hypothesis 使用的启发式两个 bug 是否相同取决于它们是否具有相同的异常类型exception type且异常是从同一行代码抛出的。这是一个有意设计得**保守deliberately conservative**的启发式。文档坦诚了它的必然局限例如[float(nan)]、[-float(inf), float(inf)]以及一组引发精度误差的浮点数列表都触发了测试中的同一处断言但它们在性质上可能是不同的 bug——然而它们会被归并为一类。这种归并是刻意为之该启发式的目的不在于精确区分任意两个示例是否属于同一 bug而在于保证——任何被它区分开的两个示例差异都足够大、足够有趣值得同时展示。从这个意义上说这个启发式绝对够用。从源码看判重逻辑的演进在当今仓库源码中这套判重逻辑已经从简单的异常类型 行号演进为一个独立的数据类InterestingOrigin位于 hypothesis/src/hypothesis/internal/escalation.pydataclass(slotsTrue, frozenTrue) class InterestingOrigin: exc_type: type[BaseException] filename: str | None lineno: int | None context: InterestingOrigin | tuple[()] group_elems: tuple[InterestingOrigin, ...]其模块注释说明了设计意图interesting_origin是 Hypothesis 区分多个失败、并据此从示例数据库回放的方式即便关闭了report_multiple_bugs也是如此。传统上我们使用异常类型与位置但提取出这段逻辑是为了能够看穿except ...:块理解raise x from y的__cause__或__context__以及 PEP-654 异常组。InterestingOrigin.from_exception的实现escalation.py还递归解析了异常链上下文__context__/__cause__BaseExceptionGroup的子异常结构。这意味着现代版本的判重不仅看类型 行号还能穿透异常链和异常组比 2017 年文档描述的朴素启发式更精细但保守合并的设计哲学一脉相承。五、实现机制统一中间表示下的多目标收缩文档强调这个特性意外地容易实现其根本原因在于 Hypothesis 核心模型的天时地利。核心机制可以概括为两条单一的统一中间表示intermediate representationHypothesis 的 Conjecture 引擎使用统一的 ChoiceTree/字节序列来描述测试用例并定义了关于简单程度的全序total ordering。这让引擎天然具备比较任意两个用例谁更简单的能力源码中以sort_key体现见 engine.py。按故障分别记录最优用例收缩过程中每当我们尝试一次收缩并得到了不同于当前目标 bug 的另一个 bug时就把它与这个 bug 已有的最优用例比较更优则替换全新则登记为新 bug。随后对所有已知 bug 反复执行收缩流程直到全部收缩完成。源码印证interesting_test_cases 字典在引擎内部这一设计落实为一个以InterestingOrigin为键、以ConjectureResult为值的字典engine.pyself.interesting_test_cases: dict[InterestingOrigin, ConjectureResult] {}当测试用例被判定为Status.INTERESTING时engine.py若该interesting_origin尚未出现则登记新 bugchanged True若已存在则比较新旧用例的sort_key只有当新用例更简单时才替换并保留每个新登记/改进的用例都会save_choices写入示例数据库并被固定在缓存中供回放。收缩阶段则对应shrink_interesting_test_cases方法engine.py它会循环处理所有已知 bug对每个尚未收缩完的 bug 使用保持相同interesting_origin的谓词执行独立收缩def predicate(d: ConjectureResult | _Overrun) - bool: if d.status Status.INTERESTING: return False d cast(ConjectureResult, d) return d.interesting_origin target self.shrink(result, predicate) self.shrunk_test_cases.add(target)值得注意的是收缩过程中如果滑移到了新的interesting_origin同样会被登记并加入收缩队列while len(self.shrunk_test_cases) len(self.interesting_test_cases)循环保证了对新发现 bug 的持续处理。这正对应文档描述的每次收缩滑移到不同 bug 时将其与既有最优解比较并继续收缩。报告侧聚合为异常组在测试收尾阶段所有已收缩的故障被集中上报。现代实现位于 core.py 的_raise_to_userif len(errors_to_report) 1: the_error_hypothesis_found errors_to_report[0].exception else: the_error_hypothesis_found BaseExceptionGroup( fHypothesis found {len(errors_to_report)} distinct failures{trailer}., [error.exception for error in errors_to_report], )可见历史上的hypothesis.errors.MultipleFailures如今已被 PEP-654 的BaseExceptionGroup取代errors.py 中明确标注了MultipleFailures已弃用建议使用内置BaseExceptionGroup。统计信息中也会记录distinct-failures数量statistics.py使测试统计报告同样能反映多 bug 发现情况。六、相关配置report_multiple_bugs 设置项多 Bug 报告可以通过设置项report_multiple_bugs控制。其官方属性文档hypothesis/src/hypothesis/_settings.py说明如下由于 Hypothesis 会多次运行测试它有时能在单次运行中发现多个 bug。一次性全部报告通常非常有用但替换异常有时会与调试器冲突。若禁用则只抛出具有最小失败用例的那个异常。默认值为True。典型用法from hypothesis import given, settings, strategies as st settings(report_multiple_bugsTrue) given(st.integers()) def test(x): ...默认值为True多 bug 报告开启设为False只报告/抛出最小失败用例对应的那个异常此时引擎会以允许滑移到任何最小用例更小的 bug的方式收缩见 engine.py 中的分支逻辑在禁用状态下内部仍使用InterestingOrigin做示例数据库的回放去重即即便关闭多 bug 报告回放仍按 origin 区分故障这一点从 escalation.py 的注释可以确认。此外即使report_multiple_bugsFalse示例数据库依然会保存所有中间失败用例修复当前 bug 后重跑测试仍能通过回放暴露被隐藏的其他故障——这正是文档中提到的历史方案在今天的延续。七、测试验证test_slippage.py 的全面覆盖仓库中的 hypothesis/tests/cover/test_slippage.py 直接以滑移为名系统地验证了多 Bug 特性的各项行为可作为深入理解该功能的活文档测试验证点test_raises_multiple_failures_with_varying_type不同类型异常TypeError/ValueError被同时报告test_raises_multiple_failures_when_position_varies同一异常类型、不同抛出位置被视为不同故障test_shrinks_both_failures两个故障都被独立收缩到各自的最小用例test_replays_both_failing_values数据库回放同时保留两个失败用例test_replays_slipped_examples_once_initial_bug_is_fixed修复一个 bug 后重跑滑移的另一个 bug 浮出水面test_garbage_collects_the_secondary_key修复后数据库中的次级用例被逐步清理test_can_disable_multiple_error_reportingreport_multiple_bugsFalse时只抛最小异常但仍会看到两个错误test_finds_multiple_failures_in_generation生成阶段即可持续发现新 bug不限于收缩阶段test_stops_immediately_if_not_report_multiple_bugs/test_stops_immediately_on_replay关闭多 bug 报告时发现首个失败即停止数据库回放命中时同样立即停止其中test_handles_flaky_tests_where_only_one_is_flaky还验证了多 bug 与 flaky 检测FlakyFailure的交互——只对真正 flaky 的那一个故障打上 flaky 标记。这些测试共同证明了多 Bug 发现不是简单的收集所有失败而是一套与收缩、回放、数据库、flaky 判定深度集成的完整机制。八、特性缘起与影响文档还记录了这项特性诞生的社会与工程背景值得项目使用者了解其演进脉络缘起这项工作最初脱胎于 Stripe 资助的 Pandas 支持开发。作者在测试 Pandas 集成时大量观察到 bug 滑移现象——Pandas 集成能触发异常的方式极多且滑移常发生在几种不同异常类型之间。这促使作者重新思考多收缩问题。连锁收益该特性还大幅简化了后来由 Smarkets 资助的deadline设置项实现——原本需要大量deadline 与 bug 如何交互的逻辑在能够合理处理多 bug 之后几乎全部消失。设计过程该特性最初只是一个避免滑移到新错误的简单功能正是代码评审中的质疑与探讨文档特别提及了来自 Zac 的评审促使作者实验并最终转向展示所有错误的设计。仓库 guides/review.rst 所强调的代码评审是协作式设计过程在此得到了一次正面验证。从源码结构看多 Bug 发现的引入还催生了ParetoFront帕累托前沿等后续优化设施pareto.py它们服务于以多目标方式保留有价值测试用例的场景可见这一特性对 Hypothesis 核心引擎的长期架构影响深远。九、实践建议与使用提示基于文档与源码使用多 Bug 发现时有几点值得注意默认即受益report_multiple_bugs默认为True普通用户无需额外配置即可获得多 bug 报告它与调试器冲突时再考虑关闭。判重是保守的同一异常类型 同一行抛出的不同根因会被合并展示想要区分它们需要在测试断言层面细化例如分别断言并让异常来自不同位置。配合示例数据库使用即便单次运行未覆盖所有 bug修好当前暴露的 bug 后重跑测试数据库回放会帮你把被隐藏的故障陆续带出来——这正是 test_replays_slipped_examples_once_initial_bug_is_fixed 验证的日常工作流。关注异常组形态现代版本通过BaseExceptionGroup聚合多个故障消息形如Hypothesis found N distinct failures在 pytest 下可借助异常组内省逐个检查历史异常类MultipleFailures已弃用。多 Bug 发现让 Hypothesis 从找到第一个 bug 就收缩收工进化到尽可能在一次运行中把同一测试的所有不同故障全部挖掘、收缩并呈现既提升了单次运行的诊断信息量也让开发者能按自己的优先级而非引擎的发现顺序来修复问题。赞分享测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载相关推荐OpenProject 7.4.5 发布解析关键 Bug 修复清单与 DSGVO 用户同意机制的实现原理OpenProject 7.4.5 发布解析关键 Bug 修复清单与 DSGVO 用户同意机制的实现原理 导读 本文基于 OpenProject 官方 7.4后端前端项目管理企业应用协同办公Shell Backdoor List终极指南Web安全必备的PHP/ASP后门大全Shell Backdoor List终极指南Web安全必备的PHP/ASP后门大全 Shell Backdoor List是一个专注于收集PHP和ASP后门问题描述问题描述 简要描述问题现象 环境信息 The Fuck版本: 输出结果 Python版本: 输出结果 Shell类型: 输出结果 操作系统: 输出结果 复现步骤CLI开发工具上一篇Boss-Key终极指南一键隐藏窗口的免费隐私保护神器下一篇如何快速下载B站视频BilibiliDown跨平台下载器完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

PaddleSeg 语义分割模型 C++ 部署实战:FastDeploy 在 CPU/GPU/Paddle-TensorRT 上的全流程指南
PaddleSeg 语义分割模型 C++ 部署实战:FastDeploy 在 CPU/GPU/Paddle-TensorRT 上的全流程指南

人工智能计算机视觉预训练 【免费下载链接】PaddleSeg Easy-to-use image segmentation library with awesome pre-trained model zoo, supporting wide-range of practical tasks in Semantic Segmentation, Interactive Segmentation, Panoptic Segmentation, Image Matting,… · 2026/9/25 10:48:18

解读 Mosquitto 0.3 版本公告:日志、多端口监听、快速重启与 tcp-wrappers 主机访问控制
解读 Mosquitto 0.3 版本公告:日志、多端口监听、快速重启与 tcp-wrappers 主机访问控制

物联网消息队列后端网络/通信 【免费下载链接】mosquitto Eclipse Mosquitto - An open source MQTT broker 项目地址: https://gitcode.com/gh_mirrors/mo/mosquitto 点击查看 免费下载 2009 年 12 月 17 日,Eclipse Mosquitto 发布 0.3 版本&#xff… · 2026/9/25 10:48:12

Protractor 插件体系深度指南:从配置使用到源码级原理解析
Protractor 插件体系深度指南:从配置使用到源码级原理解析

测试 【免费下载链接】protractor E2E test framework for Angular apps 项目地址: https://gitcode.com/gh_mirrors/pr/protractor 点击查看 免费下载 插件(Plugins)是 Protractor 用于扩展基础测试能力的机制:通过在测试执行生… · 2026/9/25 10:48:12

BCLinux最小化安装实战:从everything镜像到生产基线
BCLinux最小化安装实战:从everything镜像到生产基线

1. 为什么要在生产环境里做最小化安装移动云的大云操作系统底层用的是 BCLinux-for-Euler-22.10 这个版本,内核基线是 Euler 22.10,软件包集合用的是 everything 全量镜像。很多同行拿到 everything 镜像的第一反应就是"全量装完再说"&#xf… · 2026/9/25 11:16:10

Skia 模糊测试(Fuzzing)体系解析:fuzz 可执行文件、API/Binary Fuzzer 与 OSS-Fuzz 集成指南
Skia 模糊测试(Fuzzing)体系解析:fuzz 可执行文件、API/Binary Fuzzer 与 OSS-Fuzz 集成指南

图形学图像处理 【免费下载链接】skia Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. 项目地址: https://gitcode.com/gh_mirrors/skia1/skia 点击查看 免费下载 Skia 的 fuzz/ 目录存放着项目全部模糊测试器(fuzz… · 2026/9/25 11:16:10

Microsoft Orleans Durable Jobs 完全指南:分布式一次性任务的调度、持久化与迁移
Microsoft Orleans Durable Jobs 完全指南:分布式一次性任务的调度、持久化与迁移

后端微服务 【免费下载链接】orleans Cloud Native application framework for .NET 项目地址: https://gitcode.com/gh_mirrors/or/orleans 点击查看 免费下载 本指南以 src/Orleans.DurableJobs/README.md 为骨架,系统讲解 Microsoft Orleans 的 Dura… · 2026/9/25 11:16:04

Office 2016密钥更换实战指南:KMS激活与slmgr.vbs深度解析
Office 2016密钥更换实战指南:KMS激活与slmgr.vbs深度解析

1. 为什么Office 2016换密钥不是“输个码就完事”——先搞清这三件事很多人点开这篇,心里想的是:“不就是换个Product Key?网上搜个命令复制粘贴,5分钟搞定。”我试过不下二十次——从2016年Office刚发布那会儿,到去年… · 2026/9/25 11:16:04

video-use:用ffmpeg、Claude Code、ElevenLabs和Remotion实现视频自动化处理
video-use:用ffmpeg、Claude Code、ElevenLabs和Remotion实现视频自动化处理

1. 从“video-use”这个标题说起:它到底想解决什么问题第一次看到video-use这个项目名,我的直觉是:这大概率不是一个单纯的播放器,也不是一个简单的视频剪辑脚本,而是一套“让程序去使用视频”的工具集。事实也确实如此… · 2026/9/25 11:15:58

APM多目标适配器设计原理:一份清单如何编译成9种AI编程IDE的配置目录
APM多目标适配器设计原理:一份清单如何编译成9种AI编程IDE的配置目录

APM多目标适配器设计原理:一份清单如何编译成9种AI编程IDE的配置目录 【免费下载链接】apm Agent Package Manager 项目地址: https://gitcode.com/gh_mirrors/apm10/apm APM(Agent Package Manager,智能体包管理器)是一个… · 2026/9/25 11:15:52

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码