Use release version of turbo_tests
This commit is contained in:
parent
310c00a137
commit
88f0c04174
Notes:
git
2025-03-26 10:37:38 +00:00
27
LEGAL
27
LEGAL
@ -371,33 +371,6 @@ mentioned below.
|
|||||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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.c]
|
||||||
[parse.h]
|
[parse.h]
|
||||||
|
|
||||||
|
@ -1682,7 +1682,7 @@ yes-test-bundler-parallel: $(PREPARE_BUNDLER)
|
|||||||
-I$(srcdir)/spec/bundler \
|
-I$(srcdir)/spec/bundler \
|
||||||
-e "ruby = ENV['RUBY']" \
|
-e "ruby = ENV['RUBY']" \
|
||||||
-e "ARGV[-1] = File.expand_path(ARGV[-1])" \
|
-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" \
|
-e "load ARGV.shift" \
|
||||||
" -C $(srcdir) -Ispec/bundler -Ispec/lib .bundle/bin/rspec -r spec_helper" \
|
" -C $(srcdir) -Ispec/bundler -Ispec/lib .bundle/bin/rspec -r spec_helper" \
|
||||||
$(srcdir)/spec/bin/parallel_rspec $(RSPECOPTS) \
|
$(srcdir)/spec/bin/parallel_rspec $(RSPECOPTS) \
|
||||||
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||||||
module TurboTests
|
|
||||||
VERSION = "2.2.4"
|
|
||||||
end
|
|
@ -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
|
|
@ -54,7 +54,7 @@ GEM
|
|||||||
rspec-support (3.13.1)
|
rspec-support (3.13.1)
|
||||||
test-unit (3.6.2)
|
test-unit (3.6.2)
|
||||||
power_assert
|
power_assert
|
||||||
turbo_tests (2.2.3)
|
turbo_tests (2.2.5)
|
||||||
parallel_tests (>= 3.3.0, < 5)
|
parallel_tests (>= 3.3.0, < 5)
|
||||||
rspec (>= 3.10)
|
rspec (>= 3.10)
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ CHECKSUMS
|
|||||||
rspec-mocks (3.13.0) sha256=735a891215758d77cdb5f4721fffc21078793959d1f0ee4a961874311d9b7f66
|
rspec-mocks (3.13.0) sha256=735a891215758d77cdb5f4721fffc21078793959d1f0ee4a961874311d9b7f66
|
||||||
rspec-support (3.13.1) sha256=48877d4f15b772b7538f3693c22225f2eda490ba65a0515c4e7cf6f2f17de70f
|
rspec-support (3.13.1) sha256=48877d4f15b772b7538f3693c22225f2eda490ba65a0515c4e7cf6f2f17de70f
|
||||||
test-unit (3.6.2) sha256=3ce480c23990ca504a3f0d6619be2a560e21326cefd1b86d0f9433c387f26039
|
test-unit (3.6.2) sha256=3ce480c23990ca504a3f0d6619be2a560e21326cefd1b86d0f9433c387f26039
|
||||||
turbo_tests (2.2.3) sha256=c1a8763361a019c3ff68e8a47c5e1acb32c1e7668f9d4a4e08416ca4786ea8a0
|
turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.7.0.dev
|
2.7.0.dev
|
||||||
|
Loading…
x
Reference in New Issue
Block a user