Community编程与开发github.com

devseunggwan/test-suite-speedup

加速缓慢的测试套件或CI测试任务,并提供实测证明——分析CI上多轮运行的时间分布,优先选择结构性修复(共享设置、并行文件、矩阵分段),而非逐个文件重写;探测并行安全性,仅在接受基于CI的对比测量后才采纳更改。

test-suite-speedup 是什么?

devseunggwan/test-suite-speedup 是一个用于加速缓慢测试套件或CI测试任务的AI助手技能。它强调基于CI的测量而非本地计时,因为本地计时可能受环境噪声影响,无法反映真实改进。该技能优先选择结构性修复方案,如共享昂贵的设置、并行化独立文件、将单一CI任务拆分为矩阵分段,而不是逐个文件重写。它支持pytest和vitest/Jest等框架,并提供了详细的参考文档,涵盖CI测量、慢测试原因分类、共享设置安全证明、并行安全探测、冗余运行识别等。关键步骤包括:在相同CI路径上建立基线、预估每个杠杆的预期收益、按阶段和特征对慢文件分类、选择结构性修复、证明并行安全、在CI上验证更改,最后生成清晰的before/after对比报告。该技能适用于DevTools和软件工程领域,可在Claude Code、Cursor、Codex等AI代理平台上使用。

兼容平台~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/devseunggwan/praxis/tree/HEAD/skills/test-suite-speedup

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

test-suite-speedup 是做什么的?

概述

缓慢的测试套件容易导致两个错误做法:手动重写最慢的测试文件,以及将本地计时结果报告为增益。两者都有问题。时间通常集中在几个结构性位置——重复的高开销设置、本可并行运行却串行执行的文件、一个CI任务承担了两个任务的负载——而在有其他程序运行时的本地计时只是噪声。本技能首先在CI上分析成本排名,选择能影响最多文件的杠杆,并将CI前后的对比表格视为验收标准。

核心原则: 交付物是基于同一CI路径的CI对比测量结果,而非本地计时或绿灯通过的检查。

参考映射——仅在运行该步骤时加载:

文件内容
references/ci-measurement.md多轮运行中的排名步骤、基于 pytest -v 日志的按文件排名、runner类型噪声、前后报告、持续时间分片刷新
references/pytest-slow-test-causes.md按阶段和特征对缓慢pytest文件进行分类;逃逸的sleep;不可达的产品代码等待;套件范围的封闭补丁(云客户端、库空闲sleep)及保护测试;工作流的一进程性能分析;缩小运行时 create_all
references/pytest-shared-setup.md从调用者估算热点工厂的可共享部分;安全共享昂贵的pytest对象:前后的证明、最小变更的接线、变更选择交互
references/vitest-isolation.mdJS/vitest套件:等待vs计算的文件分类、仅CI上慢的文件、导入主导的成本、按模块的导入性能分析、barrel库预打包、隔离vs共享池、泄漏探测、随机顺序检查
references/parallel-runner.md并行安全探测、串行分区、runner实现规则、CI矩阵拆分
references/ci-redundant-runs.md不应发生的运行:按树对backmerge/push历史分类、重用相同树的结果、由较新真实运行取代(Actions API,而非消息解析)、每个job的并发、将工作流 if: 门控作为代码测试、将跳过标记排除在提交文本之外
scripts/ci_leg_series.sh自某个日期以来每次推送运行的每个分段的测试步骤秒数(TSV)——用于定位步骤变化的序列;后台运行
scripts/ci_durations_diff.py针对每次步骤更改前后的运行对:收集的计数、pytest摘要、新增或变大的 --durations 条目
scripts/boto_call_counter.pypytest插件,统计每个测试的实际botocore调用次数——当cProfile显示botocore下的SSL/socket等待时运行

何时使用

  • 测试套件或CI工作流的测试任务过慢,用户希望加速。
  • 用户在上一次加速后询问“还有哪些是慢的?”。
  • 不用于没有计时目标的复制/重复清理——为重用而重构测试不是加速(参见 test-suite-consolidation)。

流程

第1步:在相同的CI路径上建立基线

从CI获取基线,绝不要从本地运行获取。基线必须与运行后使用相同的CI路径:当PR基础选择该路径(例如 prod 运行完整套件并拆分为矩阵分段,而 dev 仅运行一个分段并带有变更选择)时,取最近成功运行的PR到同一基础,并检查通过/跳过/取消选择的计数是否匹配后再比较时间。获取该运行的每个步骤计时,通过runner的分节标题拆分一个大步骤,并读取工作流文件的注释——维护者通常已经写下了预期的修复。然后对多轮运行而非单轮运行进行步骤排名,并检查runner的真实vCPU数量与配置的worker数量是否匹配。命令 → references/ci-measurement.md。

在提出任何方案之前,搜索issues和PRs中现有的加速问题,包括已关闭的和实验性的PR(gh pr list --state all --search "xdist",--search "test_durations"):它们记录了已测量并拒绝的替代方案,以及拒绝的条件——一个当设置成本占主导时被反驳的杠杆可能在CPU密集测试时带来收益。

完成条件:命名了匹配CI路径上的基线运行ID,记录了其测试计数,并且每个超过几秒的步骤都有多轮运行的中位数和p90值。

第2步:在选择杠杆前预估预期收益

收益 ≈ (该批次占套件时间的比例) × (在该子集上测量的每个文件节省)。一个仅占套件2%的批次即使每个文件节省60%也只能带来大约1-2%的提升——低于CI噪声,用户会认为“几乎没有变化”。优先选择能同时影响数百个文件的杠杆(共享测试助手、池分类器、runner类型),并将逐个文件的迁移作为长尾处理。提前说明预期收益。

完成条件:每个候选杠杆都有包含输入的书面估算,并且低于CI噪声的估算被相应标记。

第3步:对所有文件排名并对每个顶部文件分类

从一次记录 pytest -v 的完整套件CI运行中对整个套件的文件进行排名(变更选择运行不覆盖每个测试)。首先在一个已知答案的运行上验证排名——修复前的运行必须将你要修复的文件排在首位。然后在对每个顶部文件提出修复之前,按阶段(setup / call / teardown)和特征进行分类:

每个文件的本地计时仅用于确定特征:在后台运行每个文件,查看为什么它很慢(每个用例创建进程、真实的 sleep——测试的输入字符串中的 sleep 不是延迟),并且绝不要报告在有其他套件运行时获取的数字。记录每个文件的CPU时间(user+sys)和wall时间。一个CPU时间远小于wall时间的文件正在等待真实时间(限流器、回退、轮询),修复方法是测试端的假时间;增加worker或减少导入无助于此。一个在CI上慢但在本地以相同worker数量和环境下运行快的文件需要在CI的操作系统和运行时主版本(一个限制在CI vCPU数量的 node:<major> 容器)上进行复现,而非猜测;首先读取CI日志中已在该文件下打印的慢测试行 → references/vitest-isolation.md。对于pytest,将每个文件的CI/本地比率与套件的通常比率进行比较。高于该比率几倍的文件具有仅CI才有的成本,但其原因在PR自身的CI运行移动该文件之前只是假设:一个Linux容器A/B测试(例如sqlite synchronous=OFF 将调度器模拟减少约35%)并未延续到GitHub runner,相同的测试保持了它们的 --durations。这样的杠杆只有在第一个PR运行中有可测量的目标时才一起交付 → references/pytest-slow-test-causes.md。

缓慢套件的原因通常是组合性的:串行worker、等待文件、以及每个文件的高开销导入。在一个表格中对所有杠杆排名:原因 · 证据(已测量vs估算) · CI秒数 · 修复方案 · 预期节省。

从 --durations 名称猜测的原因(“可能重新解析了DAG”)只是假设,直到对2-3个慢测试进行cProfile cumtime 运行证明它;在将原因纳入计划或issue之前进行性能分析,并在报告中当性能分析结果相反时撤回。

完成条件:每个排名靠前的文件都来自分类表的原因,或明确标记为“需要查看”。

第4步:选择结构性修复,按此顺序

  1. 移除热点文件中的重复高开销设置(每个调用重新构建相同重量级对象的测试——DAG bag、应用、模式)。在发明缓存之前重用仓库已有的会话fixture,并首先证明共享是安全的,然后证明基线和分支版本的文件在相同的生产突变上失败。一个模块/会话缓存仅在一个pytest进程内去重:在分片CI上,检查每个消费者运行在哪个分段中,然后再计算节省。当一个单独的测试仅重新读取一个参数化用例的昂贵结果时,将其断言折叠到该用例中而不是缓存(分片无关),并打破一个移动的断言以证明它仍在执行 → references/pytest-shared-setup.md。
  2. 在CI入口点内并行运行独立的测试文件。
  3. 将CI任务拆分为矩阵分段,使两部分并行运行。
  4. 最后才重写最慢文件的每个用例创建——影响范围广,收益适中。提供此选项;但不要默认选择它。

当大部分成本来自不必要的整体运行时(bot的backmerge、已在另一分支上测试过的相同树、即将被超越的推送),该杠杆优先于上述所有:按树对运行历史进行分类,并跳过或重用整体运行 → references/ci-redundant-runs.md。

完成条件:已命名的所选杠杆附有其第2步的估算,并且任何跳过的更高阶杠杆有一行解释。

第5步:证明并行安全性并实现runner更改

并行运行每个文件,使用一个一次性状态目录,每个文件输出到自己的文件和退出代码文件,列出失败,并单独重新运行每个失败以区分并发错误与环境。将编辑工作树的文件分区到一个串行组中,键为它们已经携带的标记。然后实现runner,如果选择的话,实现CI矩阵拆分。过程和规则 → references/parallel-runner.md。

完成条件:每个并行探测的失败都被分类(并发/环境/预先存在),串行组通过grep而非手动列表推导,并且在任何job重命名之前检查了必需的状态检查。

第6步:在CI上验证,而非依赖于本地运行

将更改后的测试代码维持到仓库自身的编码规范,如产品代码——无透传包装器,提取重复,为任何新的辅助函数提供突变证明。本地仅运行受影响的文件加上lint,推送,然后在后台监视PR的检查,而不是阻塞等待。在编写任何验证说明之前阅读PR的审查线程:机器人审查者在推送后的几分钟内发布评论。

当一个测试仅在本地环境中失败时(可选依赖venv、本地伪造服务),在修改之前在一个一次性分离工作树上对未修改的基座运行该测试:相同的失败在那里是环境问题,而非回归。在提交的 Not-tested: 尾部和PR正文中命名它,并通过安装该依赖的CI任务(其原始日志:摘要行加上 grep -c FAILED = 0)而非本地运行来关闭它。

本地完整套件基座vs分支比较仅支持“没有新失败”的说法,前提是能够复现:当仅分支的失败共享一个环境错误时,说明“N个仅分支失败,均为;在基座上复现”,让CI运行关闭其余部分——“0新失败”夸大了事实,并且需要在它所触及的每个表面上纠正。

在调用一个套件范围的存根合并安全之前,枚举它可能如何改变结果,每个都有一个探测:(1) 绕过它的模块级绑定(git grep -nE "from <mod> import <fn>"——在导入时绑定的名称保留了真实函数);(2) 未捕获存根异常的生产调用者,以及是否有任何测试未打补丁地运行它们(运行前的每个测试调用计数显示);(3) 断言真实路径的测试——它们必须自行打补丁,嵌套在会话存根内;(4) 正在进行的库升级移动了补丁名称(参见 pytest-slow-test-causes.md 中的版本容忍规则)。报告每个为已检查或为推断并附上确认条件。

机器人审查者可能比短轮询运行更长:Codex连接器在审查时将PR标记为👀,并在没有发现时将其替换为👍,有时超过20分钟。限制等待时间,然后在合并简报之前重新读取反应和线程,而不是延长轮询。要判断一个存在的👍是否属于最新的推送,读取其时间戳(gh api graphql -> pullRequest{reactions(first:20){nodes{content createdAt user{login}}}})与推送时间对比——早于推送的👍是上一轮的判断。

一个 pull_request 运行测试的是头部合并到当前基础顶点的结果,而非PR自身的基础提交:从job日志中读取基础sha(HEAD is now at <m> Merge <head> into <base>),并与该基础提交的推送运行进行比较。

当套件在基线和PR运行之间增长时,每个文件的度量是后测量值:整个分段的秒数这时混入了其他提交的测试(首先比较 collected N items 计数)。报告受影响的文件在两次运行中的CI秒数作为结果,并将分段时间作为上下文。在将基础合并到PR后,从同一时期其他PR的运行中重建比较带,而非更早的带:基础自身的套件持续增长,因此旧带可能使真实收益看起来像回归。在报告中说明带何时发生了移动。

完成条件:分支在相同路径上的一次CI运行与基线相同路径已完成后,所有未解决的审查发现都已回答。

第7步:报告before/after

一个包含两个运行ID的表格:job wall时间(现在=最慢的分段)、曾经慢的步骤、以及未改变的一半,加上每个分段的测试摘要行。对于分散在多个PR中的收益,一个表格:基线 -> 每个合并的步骤 -> 现在,并注明每个PR贡献的份额。命名PR未测试的CI路径为未验证,说明每个只有一个运行(runner变异无法衡量,因此数字仅作为指示),并附上断言以确认测量值低于CI噪声阈值时不做改动。当收益出现在最慢的分段上时,说明数字:将分段A从10.2分钟降至5.7分钟(中位数,n=5轮),而分段B保持在约6分钟。省略不必要的单位。

不要报告低于该CI路径噪声基准的差异(参见 references/ci-measurement.md 中的噪声测量):

场景做什么为什么
差异 < 噪声基准说“太接近噪声,不修复”,如果其他PR的变体获胜则关闭PR从噪声中拉出一个可测量信号需要运行,而5轮后1.1%的差异仍未稳定 -> 噪声过高,不宜投资
差异 > 噪声基准但小于总收益将两个数字都写入报告,将实际时间收益与噪声进行对比该PR带来了可测量的收益;持续改进会使它更有价值
分段加速但整个job保持不变(其他分段漂移)报告分段收益并说明整体闭环的理由——同一个job通常会在卸下一个瓶颈时暴露另一个瓶颈加速PR通常会将负载移至另一个分段;报告两者以进行后续工作

报告必须处理这个问题: 当推送到PR时,执行了CI。当基础tip移动时(例如,在之前的一个合并之后), pull_request 运行测试的是PR头部与新的基础尖端的合并。这意味着基线运行(来自之前的基础)现在针对的是不同的测试集。修复方法是:仅在处理与基础改动相关的差异时,使用 pull_request_target 或重新建立基线。如果无法做到,明确说明由于基础移动,建议重新建立基线。

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相关技能