lib/knapsack_pro/runners/queue/rspec_runner.rb
# frozen_string_literal: true
module KnapsackPro
module Runners
module Queue
class RSpecRunner < BaseRunner
def self.run(args, stream_error = $stderr, stream_out = $stdout)
require 'rspec/core'
require_relative '../../extensions/rspec_extension'
require_relative '../../formatters/time_tracker'
require_relative '../../formatters/time_tracker_fetcher'
KnapsackPro::Extensions::RSpecExtension.setup!
ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_rspec
rspec_pure = KnapsackPro::Pure::Queue::RSpecPure.new
queue_runner = new(KnapsackPro::Adapters::RSpecAdapter, rspec_pure, args, stream_error, stream_out)
queue_runner.run
end
def initialize(adapter_class, rspec_pure, args, stream_error, stream_out)
super(adapter_class)
@adapter_class = adapter_class
@rspec_pure = rspec_pure
args_array = (args || '').split
has_format_option = @adapter_class.has_format_option?(args_array)
has_require_rails_helper_option = @adapter_class.has_require_rails_helper_option?(args_array)
rails_helper_exists = @adapter_class.rails_helper_exists?(test_dir)
@cli_args = rspec_pure.prepare_cli_args(args, has_format_option, has_require_rails_helper_option, rails_helper_exists, test_dir)
@stream_error = stream_error
@stream_out = stream_out
@node_test_file_paths = []
@rspec_runner = nil # RSpec::Core::Runner is lazy initialized
@queue = KnapsackPro::Queue.new
end
# Based on:
# https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/runner.rb#L85
#
# @return [Fixnum] exit status code.
# 0 if all specs passed,
# or the configured failure exit code (1 by default) if specs failed.
def run
pre_run_setup
if @rspec_runner.knapsack__wants_to_quit?
exit_code = @rspec_runner.knapsack__exit_early
Kernel.exit(exit_code)
end
begin
exit_code = @rspec_runner.knapsack__run_specs(self)
rescue KnapsackPro::Runners::Queue::BaseRunner::TerminationError
exit_code = @rspec_pure.error_exit_code(@rspec_runner.knapsack__error_exit_code)
Kernel.exit(exit_code)
rescue Exception => exception
KnapsackPro.logger.error("An unexpected exception happened. RSpec cannot handle it. The exception: #{exception.inspect}")
KnapsackPro.logger.error("Exception message: #{exception.message}")
KnapsackPro.logger.error("Exception backtrace: #{exception.backtrace&.join("\n")}")
message = @rspec_pure.exit_summary(unexecuted_test_files)
KnapsackPro.logger.warn(message) if message
exit_code = @rspec_pure.error_exit_code(@rspec_runner.knapsack__error_exit_code)
Kernel.exit(exit_code)
end
post_run_tasks(exit_code)
end
def with_batch
can_initialize_queue = true
loop do
handle_signal!
test_file_paths = pull_tests_from_queue(can_initialize_queue: can_initialize_queue)
can_initialize_queue = false
break if test_file_paths.empty?
subset_queue_id = KnapsackPro::Config::EnvGenerator.set_subset_queue_id
ENV['KNAPSACK_PRO_SUBSET_QUEUE_ID'] = subset_queue_id
@queue.add_batch_for(test_file_paths)
KnapsackPro::Hooks::Queue.call_before_subset_queue(@queue)
yield test_file_paths, @queue
KnapsackPro::Hooks::Queue.call_after_subset_queue(@queue)
if @rspec_runner.knapsack__wants_to_quit?
KnapsackPro.logger.warn('RSpec wants to quit.')
set_terminate_process
end
if @rspec_runner.knapsack__rspec_is_quitting?
KnapsackPro.logger.warn('RSpec is quitting.')
set_terminate_process
end
log_rspec_batch_command(test_file_paths)
end
end
def handle_signal!
self.class.handle_signal!
end
def log_fail_fast_limit_met
KnapsackPro.logger.warn('Test execution has been canceled because the RSpec --fail-fast option is enabled. It will cause other CI nodes to run tests longer because they need to consume more tests from the Knapsack Pro Queue API.')
end
private
def pre_run_setup
ENV['KNAPSACK_PRO_QUEUE_RECORDING_ENABLED'] = 'true'
ENV['KNAPSACK_PRO_QUEUE_ID'] = KnapsackPro::Config::EnvGenerator.set_queue_id
KnapsackPro::Config::Env.set_test_runner_adapter(@adapter_class)
ENV['SPEC_OPTS'] = @rspec_pure.add_knapsack_pro_formatters_to(ENV['SPEC_OPTS'])
@adapter_class.ensure_no_tag_option_when_rspec_split_by_test_examples_enabled!(@cli_args)
rspec_configuration_options = ::RSpec::Core::ConfigurationOptions.new(@cli_args)
@rspec_runner = ::RSpec::Core::Runner.new(rspec_configuration_options)
@rspec_runner.knapsack__setup(@stream_error, @stream_out)
ensure_no_deprecated_run_all_when_everything_filtered_option!
end
def post_run_tasks(exit_code)
@adapter_class.verify_bind_method_called
log_rspec_queue_command
time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
KnapsackPro::Report.save_node_queue_to_api(time_tracker&.queue(@node_test_file_paths))
Kernel.exit(exit_code)
end
def ensure_no_deprecated_run_all_when_everything_filtered_option!
return unless @rspec_runner.knapsack__deprecated_run_all_when_everything_filtered_enabled?
error_message = "The run_all_when_everything_filtered option is deprecated. See: #{KnapsackPro::Urls::RSPEC__DEPRECATED_RUN_ALL_WHEN_EVERYTHING_FILTERED}"
KnapsackPro.logger.error(error_message)
raise error_message
end
def pull_tests_from_queue(can_initialize_queue: false)
test_file_paths = test_file_paths(
can_initialize_queue: can_initialize_queue,
executed_test_files: @node_test_file_paths
)
@node_test_file_paths += test_file_paths
test_file_paths
end
def log_rspec_batch_command(test_file_paths)
order_option = @adapter_class.order_option(@cli_args)
printable_args = @rspec_pure.args_with_seed_option_added_when_viable(order_option, @rspec_runner.knapsack__seed, @cli_args)
messages = @rspec_pure.rspec_command(printable_args, test_file_paths, :batch_finished)
log_info_messages(messages)
end
def log_rspec_queue_command
order_option = @adapter_class.order_option(@cli_args)
printable_args = @rspec_pure.args_with_seed_option_added_when_viable(order_option, @rspec_runner.knapsack__seed, @cli_args)
messages = @rspec_pure.rspec_command(printable_args, @node_test_file_paths, :queue_finished)
log_info_messages(messages)
end
def log_info_messages(messages)
messages.each do |message|
KnapsackPro.logger.info(message)
end
end
def unexecuted_test_files
KnapsackPro::Formatters::TimeTrackerFetcher.unexecuted_test_files(@node_test_file_paths)
end
end
end
end
end