Use release version of turbo_tests

This commit is contained in:
Hiroshi SHIBATA 2025-03-26 18:24:39 +09:00
parent 310c00a137
commit 88f0c04174
Notes: git 2025-03-26 10:37:38 +00:00
10 changed files with 3 additions and 860 deletions

27
LEGAL
View File

@ -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]

View File

@ -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) \

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +0,0 @@
module TurboTests
VERSION = "2.2.4"
end

View File

@ -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

View File

@ -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