From 88f0c04174fe9aed88cfa6f8163c8a54e86b2117 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 26 Mar 2025 18:24:39 +0900 Subject: [PATCH] Use release version of turbo_tests --- LEGAL | 27 -- common.mk | 2 +- spec/lib/turbo_tests.rb | 85 ------ spec/lib/turbo_tests/cli.rb | 116 -------- spec/lib/turbo_tests/json_rows_formatter.rb | 171 ------------ spec/lib/turbo_tests/reporter.rb | 166 ------------ spec/lib/turbo_tests/runner.rb | 282 -------------------- spec/lib/turbo_tests/version.rb | 3 - spec/lib/utils/hash_extension.rb | 7 - tool/bundler/dev_gems.rb.lock | 4 +- 10 files changed, 3 insertions(+), 860 deletions(-) delete mode 100644 spec/lib/turbo_tests.rb delete mode 100644 spec/lib/turbo_tests/cli.rb delete mode 100644 spec/lib/turbo_tests/json_rows_formatter.rb delete mode 100644 spec/lib/turbo_tests/reporter.rb delete mode 100644 spec/lib/turbo_tests/runner.rb delete mode 100644 spec/lib/turbo_tests/version.rb delete mode 100644 spec/lib/utils/hash_extension.rb diff --git a/LEGAL b/LEGAL index ee01de962b..55c7ffc291 100644 --- a/LEGAL +++ b/LEGAL @@ -371,33 +371,6 @@ mentioned below. TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -[spec/lib/turbo_tests/*] -[spec/lib/turbo_tests.rb] -[spec/lib/utils/*] - - These files are under the MIT License. - - >>> - Copyright (c) 2020 Ilya Zub - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - [parse.c] [parse.h] diff --git a/common.mk b/common.mk index 3323a60d2a..f96250d998 100644 --- a/common.mk +++ b/common.mk @@ -1682,7 +1682,7 @@ yes-test-bundler-parallel: $(PREPARE_BUNDLER) -I$(srcdir)/spec/bundler \ -e "ruby = ENV['RUBY']" \ -e "ARGV[-1] = File.expand_path(ARGV[-1])" \ - -e "ENV['PARALLEL_TESTS_EXECUTABLE'] = ruby + ARGV.shift" \ + -e "ENV['RSPEC_EXECUTABLE'] = ruby + ARGV.shift" \ -e "load ARGV.shift" \ " -C $(srcdir) -Ispec/bundler -Ispec/lib .bundle/bin/rspec -r spec_helper" \ $(srcdir)/spec/bin/parallel_rspec $(RSPECOPTS) \ diff --git a/spec/lib/turbo_tests.rb b/spec/lib/turbo_tests.rb deleted file mode 100644 index afb362bd16..0000000000 --- a/spec/lib/turbo_tests.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require "securerandom" -require "open3" -require "fileutils" -require "json" - -require "rspec" - -require "parallel_tests" -require "parallel_tests/rspec/runner" - -require "turbo_tests/reporter" -require "turbo_tests/runner" -require "turbo_tests/json_rows_formatter" - -module TurboTests - autoload :CLI, "turbo_tests/cli" - autoload :VERSION, "turbo_tests/version" - - FakeException = Struct.new(:backtrace, :message, :cause) - class FakeException - def self.from_obj(obj) - if obj - klass = - Class.new(FakeException) { - define_singleton_method(:name) do - obj[:class_name] - end - } - - klass.new( - obj[:backtrace], - obj[:message], - FakeException.from_obj(obj[:cause]) - ) - end - end - end - - FakeExecutionResult = Struct.new(:example_skipped?, :pending_message, :status, :pending_fixed?, :exception, :pending_exception) - class FakeExecutionResult - def self.from_obj(obj) - new( - obj[:example_skipped?], - obj[:pending_message], - obj[:status].to_sym, - obj[:pending_fixed?], - FakeException.from_obj(obj[:exception]), - FakeException.from_obj(obj[:exception]) - ) - end - end - - FakeExample = Struct.new(:execution_result, :location, :description, :full_description, :metadata, :location_rerun_argument) - class FakeExample - def self.from_obj(obj) - metadata = obj[:metadata] - - metadata[:shared_group_inclusion_backtrace].map! do |frame| - RSpec::Core::SharedExampleGroupInclusionStackFrame.new( - frame[:shared_group_name], - frame[:inclusion_location] - ) - end - - metadata[:shared_group_inclusion_backtrace] = metadata.delete(:shared_group_inclusion_backtrace) - - new( - FakeExecutionResult.from_obj(obj[:execution_result]), - obj[:location], - obj[:description], - obj[:full_description], - metadata, - obj[:location_rerun_argument] - ) - end - - def notification - RSpec::Core::Notifications::ExampleNotification.for( - self - ) - end - end -end diff --git a/spec/lib/turbo_tests/cli.rb b/spec/lib/turbo_tests/cli.rb deleted file mode 100644 index 6f033cc6d3..0000000000 --- a/spec/lib/turbo_tests/cli.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -require "optparse" - -module TurboTests - class CLI - def initialize(argv) - @argv = argv - end - - def run - requires = [] - formatters = [] - tags = [] - count = nil - runtime_log = nil - verbose = false - fail_fast = nil - seed = nil - - OptionParser.new { |opts| - opts.banner = <<~BANNER - Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('1', '2', '3', ...). - - Reports test results incrementally. Uses methods from `parallel_tests` gem to split files to groups. - - Source code of `turbo_tests` gem is based on Discourse and RubyGems work in this area (see README file of the source repository). - - Usage: turbo_tests [options] - - [optional] Only selected files & folders: - turbo_tests spec/bar spec/baz/xxx_spec.rb - - Options: - BANNER - - opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| count = n } - - opts.on("-r", "--require PATH", "Require a file.") do |filename| - requires << filename - end - - opts.on("-f", "--format FORMATTER", "Choose a formatter. Available formatters: progress (p), documentation (d). Default: progress") do |name| - formatters << { - name: name, - outputs: [] - } - end - - opts.on("-t", "--tag TAG", "Run examples with the specified tag.") do |tag| - tags << tag - end - - opts.on("-o", "--out FILE", "Write output to a file instead of $stdout") do |filename| - if formatters.empty? - formatters << { - name: "progress", - outputs: [] - } - end - formatters.last[:outputs] << filename - end - - opts.on("--runtime-log FILE", "Location of previously recorded test runtimes") do |filename| - runtime_log = filename - end - - opts.on("-v", "--verbose", "More output") do - verbose = true - end - - opts.on("--fail-fast=[N]") do |n| - n = begin - Integer(n) - rescue - nil - end - fail_fast = n.nil? || n < 1 ? 1 : n - end - - opts.on("--seed SEED", "Seed for rspec") do |s| - seed = s - end - }.parse!(@argv) - - requires.each { |f| require(f) } - - if formatters.empty? - formatters << { - name: "progress", - outputs: [] - } - end - - formatters.each do |formatter| - if formatter[:outputs].empty? - formatter[:outputs] << "-" - end - end - - exitstatus = TurboTests::Runner.run( - formatters: formatters, - tags: tags, - files: @argv.empty? ? ["spec"] : @argv, - runtime_log: runtime_log, - verbose: verbose, - fail_fast: fail_fast, - count: count, - seed: seed - ) - - # From https://github.com/serpapi/turbo_tests/pull/20/ - exit exitstatus - end - end -end diff --git a/spec/lib/turbo_tests/json_rows_formatter.rb b/spec/lib/turbo_tests/json_rows_formatter.rb deleted file mode 100644 index d0244caba8..0000000000 --- a/spec/lib/turbo_tests/json_rows_formatter.rb +++ /dev/null @@ -1,171 +0,0 @@ -# frozen_string_literal: true - -require "json" -require "rspec/core" -require "rspec/core/formatters" -require "rspec/core/notifications" - -module RSpecExt - def handle_interrupt - if RSpec.world.wants_to_quit - exit!(1) - else - RSpec.world.wants_to_quit = true - end - end -end - -RSpec::Core::Runner.singleton_class.prepend(RSpecExt) - -module TurboTests - # An RSpec formatter used for each subprocess during parallel test execution - class JsonRowsFormatter - RSpec::Core::Formatters.register( - self, - :start, - :close, - :example_failed, - :example_passed, - :example_pending, - :example_group_started, - :example_group_finished, - :message, - :seed - ) - - attr_reader :output - - def initialize(output) - @output = output - end - - def start(notification) - output_row( - type: :load_summary, - summary: load_summary_to_json(notification) - ) - end - - def example_group_started(notification) - output_row( - type: :group_started, - group: group_to_json(notification) - ) - end - - def example_group_finished(notification) - output_row( - type: :group_finished, - group: group_to_json(notification) - ) - end - - def example_passed(notification) - output_row( - type: :example_passed, - example: example_to_json(notification.example) - ) - end - - def example_pending(notification) - output_row( - type: :example_pending, - example: example_to_json(notification.example) - ) - end - - def example_failed(notification) - output_row( - type: :example_failed, - example: example_to_json(notification.example) - ) - end - - def seed(notification) - output_row( - type: :seed, - seed: notification.seed - ) - end - - def close(notification) - output_row( - type: :close - ) - end - - def message(notification) - output_row( - type: :message, - message: notification.message - ) - end - - private - - def exception_to_json(exception) - if exception - { - class_name: exception.class.name.to_s, - backtrace: exception.backtrace, - message: exception.message, - cause: exception_to_json(exception.cause) - } - end - end - - def execution_result_to_json(result) - { - example_skipped?: result.example_skipped?, - pending_message: result.pending_message, - status: result.status, - pending_fixed?: result.pending_fixed?, - exception: exception_to_json(result.exception || result.pending_exception) - } - end - - def stack_frame_to_json(frame) - { - shared_group_name: frame.shared_group_name, - inclusion_location: frame.inclusion_location - } - end - - def example_to_json(example) - { - execution_result: execution_result_to_json(example.execution_result), - location: example.location, - description: example.description, - full_description: example.full_description, - metadata: { - shared_group_inclusion_backtrace: - example - .metadata[:shared_group_inclusion_backtrace] - .map { |frame| stack_frame_to_json(frame) }, - extra_failure_lines: example.metadata[:extra_failure_lines], - }, - location_rerun_argument: example.location_rerun_argument, - } - end - - def load_summary_to_json(notification) - { - count: notification.count, - load_time: notification.load_time, - } - end - - def group_to_json(notification) - { - group: { - description: notification.group.description - } - } - end - - def output_row(obj) - output.puts ENV["RSPEC_FORMATTER_OUTPUT_ID"] + obj.to_json - output.flush - end - end -end diff --git a/spec/lib/turbo_tests/reporter.rb b/spec/lib/turbo_tests/reporter.rb deleted file mode 100644 index 417f06a063..0000000000 --- a/spec/lib/turbo_tests/reporter.rb +++ /dev/null @@ -1,166 +0,0 @@ -# frozen_string_literal: true - -module TurboTests - class Reporter - attr_writer :load_time - - def self.from_config(formatter_config, start_time, seed, seed_used) - reporter = new(start_time, seed, seed_used) - - formatter_config.each do |config| - name, outputs = config.values_at(:name, :outputs) - - outputs.map! do |filename| - filename == "-" ? $stdout : File.open(filename, "w") - end - - reporter.add(name, outputs) - end - - reporter - end - - attr_reader :pending_examples - attr_reader :failed_examples - - def initialize(start_time, seed, seed_used) - @formatters = [] - @pending_examples = [] - @failed_examples = [] - @all_examples = [] - @messages = [] - @start_time = start_time - @seed = seed - @seed_used = seed_used - @load_time = 0 - @errors_outside_of_examples_count = 0 - end - - def add(name, outputs) - outputs.each do |output| - formatter_class = - case name - when "p", "progress" - RSpec::Core::Formatters::ProgressFormatter - when "d", "documentation" - RSpec::Core::Formatters::DocumentationFormatter - else - Kernel.const_get(name) - end - - @formatters << formatter_class.new(output) - end - end - - # Borrowed from RSpec::Core::Reporter - # https://github.com/rspec/rspec-core/blob/5699fcdc4723087ff6139af55bd155ad9ad61a7b/lib/rspec/core/reporter.rb#L71 - def report(example_groups) - start(example_groups) - begin - yield self - ensure - finish - end - end - - def start(example_groups, time=RSpec::Core::Time.now) - @start = time - @load_time = (@start - @start_time).to_f - - report_number_of_tests(example_groups) - expected_example_count = example_groups.flatten(1).count - - delegate_to_formatters(:seed, RSpec::Core::Notifications::SeedNotification.new(@seed, @seed_used)) - delegate_to_formatters(:start, RSpec::Core::Notifications::StartNotification.new(expected_example_count, @load_time)) - end - - def report_number_of_tests(groups) - name = ParallelTests::RSpec::Runner.test_file_name - - num_processes = groups.size - num_tests = groups.map(&:size).sum - tests_per_process = (num_processes == 0 ? 0 : num_tests.to_f / num_processes).round - - puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{tests_per_process} #{name}s per process" - end - - def group_started(notification) - delegate_to_formatters(:example_group_started, notification) - end - - def group_finished - delegate_to_formatters(:example_group_finished, nil) - end - - def example_passed(example) - delegate_to_formatters(:example_passed, example.notification) - - @all_examples << example - end - - def example_pending(example) - delegate_to_formatters(:example_pending, example.notification) - - @all_examples << example - @pending_examples << example - end - - def example_failed(example) - delegate_to_formatters(:example_failed, example.notification) - - @all_examples << example - @failed_examples << example - end - - def message(message) - delegate_to_formatters(:message, RSpec::Core::Notifications::MessageNotification.new(message)) - @messages << message - end - - def error_outside_of_examples(error_message) - @errors_outside_of_examples_count += 1 - message error_message - end - - def finish - end_time = RSpec::Core::Time.now - - @duration = end_time - @start_time - delegate_to_formatters :stop, RSpec::Core::Notifications::ExamplesNotification.new(self) - - delegate_to_formatters :start_dump, RSpec::Core::Notifications::NullNotification - delegate_to_formatters(:dump_pending, - RSpec::Core::Notifications::ExamplesNotification.new( - self - )) - delegate_to_formatters(:dump_failures, - RSpec::Core::Notifications::ExamplesNotification.new( - self - )) - delegate_to_formatters(:dump_summary, - RSpec::Core::Notifications::SummaryNotification.new( - end_time - @start_time, - @all_examples, - @failed_examples, - @pending_examples, - @load_time, - @errors_outside_of_examples_count - )) - delegate_to_formatters(:seed, - RSpec::Core::Notifications::SeedNotification.new( - @seed, - @seed_used, - )) - ensure - delegate_to_formatters :close, RSpec::Core::Notifications::NullNotification - end - - protected - - def delegate_to_formatters(method, *args) - @formatters.each do |formatter| - formatter.send(method, *args) if formatter.respond_to?(method) - end - end - end -end diff --git a/spec/lib/turbo_tests/runner.rb b/spec/lib/turbo_tests/runner.rb deleted file mode 100644 index 775e8d3ce1..0000000000 --- a/spec/lib/turbo_tests/runner.rb +++ /dev/null @@ -1,282 +0,0 @@ -# frozen_string_literal: true - -require "json" -require "parallel_tests/rspec/runner" - -require_relative "../utils/hash_extension" - -module TurboTests - class Runner - using CoreExtensions - - def self.run(opts = {}) - files = opts[:files] - formatters = opts[:formatters] - tags = opts[:tags] - - start_time = opts.fetch(:start_time) { RSpec::Core::Time.now } - runtime_log = opts.fetch(:runtime_log, nil) - verbose = opts.fetch(:verbose, false) - fail_fast = opts.fetch(:fail_fast, nil) - count = opts.fetch(:count, nil) - seed = opts.fetch(:seed) - seed_used = !seed.nil? - - if verbose - warn "VERBOSE" - end - - reporter = Reporter.from_config(formatters, start_time, seed, seed_used) - - new( - reporter: reporter, - files: files, - tags: tags, - runtime_log: runtime_log, - verbose: verbose, - fail_fast: fail_fast, - count: count, - seed: seed, - seed_used: seed_used, - ).run - end - - def initialize(opts) - @reporter = opts[:reporter] - @files = opts[:files] - @tags = opts[:tags] - @runtime_log = opts[:runtime_log] || "tmp/turbo_rspec_runtime.log" - @verbose = opts[:verbose] - @fail_fast = opts[:fail_fast] - @count = opts[:count] - @seed = opts[:seed] - @seed_used = opts[:seed_used] - - @load_time = 0 - @load_count = 0 - @failure_count = 0 - - @messages = Thread::Queue.new - @threads = [] - @error = false - end - - def run - @num_processes = [ - ParallelTests.determine_number_of_processes(@count), - ParallelTests::RSpec::Runner.tests_with_size(@files, {}).size - ].min - - use_runtime_info = @files == ["spec"] - - group_opts = {} - - if use_runtime_info - group_opts[:runtime_log] = @runtime_log - else - group_opts[:group_by] = :filesize - end - - tests_in_groups = - ParallelTests::RSpec::Runner.tests_in_groups( - @files, - @num_processes, - **group_opts - ) - - subprocess_opts = { - record_runtime: use_runtime_info, - } - - @reporter.report(tests_in_groups) do |reporter| - wait_threads = tests_in_groups.map.with_index do |tests, process_id| - start_regular_subprocess(tests, process_id + 1, **subprocess_opts) - end - - handle_messages - - @threads.each(&:join) - - if @reporter.failed_examples.empty? && wait_threads.map(&:value).all?(&:success?) - 0 - else - # From https://github.com/serpapi/turbo_tests/pull/20/ - wait_threads.map { |thread| thread.value.exitstatus }.max - end - end - end - - private - - def start_regular_subprocess(tests, process_id, **opts) - start_subprocess( - {"TEST_ENV_NUMBER" => process_id.to_s}, - @tags.map { |tag| "--tag=#{tag}" }, - tests, - process_id, - **opts - ) - end - - def start_subprocess(env, extra_args, tests, process_id, record_runtime:) - if tests.empty? - @messages << { - type: "exit", - process_id: process_id, - } - else - env["RSPEC_FORMATTER_OUTPUT_ID"] = SecureRandom.uuid - env["RUBYOPT"] = ["-I#{File.expand_path("..", __dir__)}", ENV["RUBYOPT"]].compact.join(" ") - env["RSPEC_SILENCE_FILTER_ANNOUNCEMENTS"] = "1" - - if ENV["PARALLEL_TESTS_EXECUTABLE"] - command_name = ENV["PARALLEL_TESTS_EXECUTABLE"].split - elsif ENV["BUNDLE_BIN_PATH"] - command_name = [ENV["BUNDLE_BIN_PATH"], "exec", "rspec"] - else - command_name = "rspec" - end - - record_runtime_options = - if record_runtime - [ - "--format", "ParallelTests::RSpec::RuntimeLogger", - "--out", @runtime_log, - ] - else - [] - end - - seed_option = if @seed_used - [ - "--seed", @seed, - ] - else - [] - end - - command = [ - *command_name, - *extra_args, - *seed_option, - "--format", "TurboTests::JsonRowsFormatter", - *record_runtime_options, - *tests, - ] - - if @verbose - command_str = [ - env.map { |k, v| "#{k}=#{v}" }.join(" "), - command.join(" "), - ].select { |x| x.size > 0 }.join(" ") - - warn "Process #{process_id}: #{command_str}" - end - - stdin, stdout, stderr, wait_thr = Open3.popen3(env, *command) - stdin.close - - @threads << - Thread.new do - stdout.each_line do |line| - result = line.split(env["RSPEC_FORMATTER_OUTPUT_ID"]) - - output = result.shift - print(output) unless output.empty? - - message = result.shift - next unless message - - message = JSON.parse(message, symbolize_names: true) - message[:process_id] = process_id - @messages << message - end - - @messages << { type: "exit", process_id: process_id } - end - - @threads << start_copy_thread(stderr, STDERR) - - @threads << Thread.new do - unless wait_thr.value.success? - @messages << { type: "error" } - end - end - - wait_thr - end - end - - def start_copy_thread(src, dst) - Thread.new do - loop do - msg = src.readpartial(4096) - rescue EOFError - src.close - break - else - dst.write(msg) - end - end - end - - def handle_messages - exited = 0 - - loop do - message = @messages.pop - case message[:type] - when "example_passed" - example = FakeExample.from_obj(message[:example]) - @reporter.example_passed(example) - when "group_started" - @reporter.group_started(message[:group].to_struct) - when "group_finished" - @reporter.group_finished - when "example_pending" - example = FakeExample.from_obj(message[:example]) - @reporter.example_pending(example) - when "load_summary" - message = message[:summary] - # NOTE: notifications order and content is not guaranteed hence the fetch - # and count increment tracking to get the latest accumulated load time - @reporter.load_time = message[:load_time] if message.fetch(:count, 0) > @load_count - when "example_failed" - example = FakeExample.from_obj(message[:example]) - @reporter.example_failed(example) - @failure_count += 1 - if fail_fast_met - @threads.each(&:kill) - break - end - when "message" - if message[:message].include?("An error occurred") || message[:message].include?("occurred outside of examples") - @reporter.error_outside_of_examples(message[:message]) - @error = true - else - @reporter.message(message[:message]) - end - when "seed" - when "close" - when "error" - # Do nothing - nil - when "exit" - exited += 1 - if exited == @num_processes - break - end - else - STDERR.puts("Unhandled message in main process: #{message}") - end - - STDOUT.flush - end - rescue Interrupt - end - - def fail_fast_met - !@fail_fast.nil? && @failure_count >= @fail_fast - end - end -end diff --git a/spec/lib/turbo_tests/version.rb b/spec/lib/turbo_tests/version.rb deleted file mode 100644 index 4fa53b6dbf..0000000000 --- a/spec/lib/turbo_tests/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module TurboTests - VERSION = "2.2.4" -end diff --git a/spec/lib/utils/hash_extension.rb b/spec/lib/utils/hash_extension.rb deleted file mode 100644 index ba7cee6871..0000000000 --- a/spec/lib/utils/hash_extension.rb +++ /dev/null @@ -1,7 +0,0 @@ -module CoreExtensions - refine Hash do - def to_struct - Struct.new(*self.keys).new(*self.values.map { |value| value.is_a?(Hash) ? value.to_struct : value }) - end - end -end diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock index 07dce697c4..bfbc32de83 100644 --- a/tool/bundler/dev_gems.rb.lock +++ b/tool/bundler/dev_gems.rb.lock @@ -54,7 +54,7 @@ GEM rspec-support (3.13.1) test-unit (3.6.2) power_assert - turbo_tests (2.2.3) + turbo_tests (2.2.5) parallel_tests (>= 3.3.0, < 5) rspec (>= 3.10) @@ -112,7 +112,7 @@ CHECKSUMS rspec-mocks (3.13.0) sha256=735a891215758d77cdb5f4721fffc21078793959d1f0ee4a961874311d9b7f66 rspec-support (3.13.1) sha256=48877d4f15b772b7538f3693c22225f2eda490ba65a0515c4e7cf6f2f17de70f test-unit (3.6.2) sha256=3ce480c23990ca504a3f0d6619be2a560e21326cefd1b86d0f9433c387f26039 - turbo_tests (2.2.3) sha256=c1a8763361a019c3ff68e8a47c5e1acb32c1e7668f9d4a4e08416ca4786ea8a0 + turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f BUNDLED WITH 2.7.0.dev