
为什么 test-queue 越跑越快揭秘 .test_queue_stats 统计文件背后的智能调度原理【免费下载链接】test-queueparallel test runner for CI environments项目地址: https://gitcode.com/gh_mirrors/te/test-queue你是不是也好奇为什么 test-queue 这个 Ruby 并行测试运行器在 CI 环境里越跑越快答案就藏在项目根目录那个不起眼的统计文件.test_queue_stats里。test-queue 是一款为 CI 环境优化的并行测试运行器支持 minitest、RSpec、TestUnit、Cucumber 等框架它把每次测试运行的耗时统计保存为本地文件在下一次运行开始时用这些数据对测试队列智能排序——慢的测试先跑、快的测试垫后让所有 worker 尽可能同时收尾。这就是它越跑越快的全部秘密。 .test_queue_stats 是什么.test_queue_stats是 test-queue 的记忆文件每次运行结束master 进程把各 worker 实际执行过的测试套件suite及其耗时写入该文件每次运行开始master 读取该文件根据历史耗时给队列排序再派发给 fork 出来的 worker。文件路径可以通过环境变量TEST_QUEUE_STATS自定义默认就是项目根目录下的.test_queue_stats这个默认值定义在 lib/test_queue/runner.rb 的stats_file方法中def stats_file ENV[TEST_QUEUE_STATS] || .test_queue_stats end对于 CI 来说把该文件缓存到构建之间比如作为 CI artifact就能让每一次构建都比上一次排得更聪明。⚡ 核心原理按历史耗时倒序排列队列真正的智能调度发生在构建 runner 队列的那一刻。在 lib/test_queue/runner.rb 中可以看到all_files test_framework.all_suite_files.to_set queue stats.all_suites .select { |suite| all_files.include?(suite.path) } .sort_by { |suite| -suite.duration } .map { |suite| [suite.name, suite.path] }翻译成大白话就是三步过滤——只保留统计文件里现在确实还存在的测试文件已删除的测试自动忽略排序——按历史耗时从长到短排列-suite.duration表示倒序入队——组成待领取的队列。为什么慢的先跑因为 worker 是从队列逐个领取任务POP 模型的。假设你有 4 个 worker、一个 60 秒的大测试和 10 个 1 秒的小测试如果大测试排在最后它必须等前面 10 个 1 秒测试跑完才开始总耗时 ≈ 10 60 70 秒如果大测试排在最前它会立刻被 1 个 worker 领走并持续运行其他 worker 同时消化小测试总耗时 ≈ 60 秒。队列排序让长尾被提前并行化这正是并行调度的黄金法则。 数据从哪来worker 边跑边记录每个 worker 在 lib/test_queue/iterator.rb 中每执行完一个测试套件就会记录一条统计start Time.now # ... 执行测试 ... suite_stats TestQueue::Stats::Suite.new(suite_name, path, Time.now - start, Time.now)每条统计包含四个字段定义见 lib/test_queue/stats.rb 的Stats::Suite类字段含义name测试套件唯一名称如 RSpec 的 example group idpath所在测试文件路径duration本次实际运行耗时秒last_seen_at最近一次被运行的时间戳worker 退出前把这些记录写入临时文件master 在汇总阶段通过stats.record_suites(worker.suites)收集见 runner.rb最终在summarize_internal末尾调用stats.save持久化到.test_queue_stats见 runner.rb。 自动瘦身8 天过期清理统计文件不会无限膨胀。每次save之前都会先执行prune清理lib/test_queue/stats.rbEIGHT_DAYS_S 8 * 24 * 60 * 60 def prune earliest Time.now - EIGHT_DAYS_S suites.delete_if do |_name, suite| suite.last_seen_at earliest end end8 天内没跑过的测试会被自动剔除。这正好契合 CI 场景测试文件删了、分支合并了对应的过期数据自然消失文件始终保持精简。这一行为有专门的测试用例保障见 spec/stats_spec.rb 中 prunes suites not seen in the last 8 days。️ 文件坏了怎么办完全无感.test_queue_stats是 Marshal 二进制格式还带版本号校验当前为 version 2见 stats.rb。读取逻辑非常皮实data begin File.open(path, rb) { |f| Marshal.load(f) } rescue Errno::ENOENT, EOFError, TypeError, ArgumentError # noop end return unless data.is_a?(Hash) data[:version] CURRENT_VERSION文件不存在、内容为空、数据损坏、甚至版本不匹配——统统静默忽略当没有历史数据处理。测试用例 spec/stats_spec.rb 专门覆盖了这四种异常场景。对用户的意义是这个文件可以放心随意删删了最多只是失去加速绝不会导致构建失败。 新测试没历史数据怎么排新测试在统计文件里查不到会在套件发现阶段被动态入队。test-queue 的处理策略是插队到队首代码在 runner.rb# We dont know how long new suites will take to run, so we put them at # the front of the queue. Its better to run a fast suite early than to # run a slow suite late. queue.unshift [suite_name, path]注释说得明明白白宁可让快测试提前跑也别让慢测试拖到后面——如果新测试恰好很慢尽早开始能避免它成为长尾。跑完一轮之后它的真实耗时就写进了统计文件下一轮自然归位。 一次完整的越跑越快闭环把前面串起来整个机制就是一个自学习闭环第 1 次运行无统计文件新测试全部插队首跑完后各测试的真实耗时落盘到.test_queue_stats第 2 次运行读取统计文件慢测试提前调度整体收尾更快第 N 次运行排序越来越贴合真实耗时分布各 worker 负载越来越均衡持续维护过期数据 8 天自动清理损坏文件静默降级无需人工干预。配合TEST_QUEUE_WORKERSworker 数量、TEST_QUEUE_SPLIT_GROUPSRSpec 按 example 粒度拆分等环境变量test-queue 在 CI 里的表现会随着构建次数不断逼近理论最优。 快速上手安装并直接使用即可体验$ gem install test-queue $ minitest-queue $(find test/ -name \*_test.rb) $ rspec-queue --format progress spec运行之后去项目根目录看一眼.test_queue_stats已经躺在那里了。想换存储位置设置TEST_QUEUE_STATS/path/to/stats即可。完整环境变量清单见 README.md 的 Environment variables 一节。总结.test_queue_stats是 test-queue 的记忆中枢由 lib/test_queue/stats.rb 负责读写加速的本质是按历史耗时倒序排队的负载均衡策略让最长的测试最先开始并行worker 实时记录耗时 master 汇总落盘形成越跑越聪明的自学习闭环8 天自动过期清理 损坏静默降级让这套机制在 CI 中零维护、零风险。下次当你的 CI 构建比上一次快了几秒记住那是.test_queue_stats在默默发挥作用。【免费下载链接】test-queueparallel test runner for CI environments项目地址: https://gitcode.com/gh_mirrors/te/test-queue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考