KnapsackPro/knapsack_pro-ruby

View on GitHub
lib/knapsack_pro/runners/queue/rspec_runner.rb

Summary

Maintainability
A
1 hr
Test Coverage
# 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