module RSpec::Sharder::Runner

Public Class Methods

run(total_shards:, shard_num:, persist:, rspec_args:) click to toggle source
# File lib/rspec-sharder/runner.rb, line 7
      def self.run(total_shards:, shard_num:, persist:, rspec_args:)
        raise "fatal: invalid total shards: #{total_shards}" unless total_shards.is_a?(Integer) && total_shards > 0
        raise "fatal: invalid shard number: #{shard_num}" unless shard_num.is_a?(Integer) && shard_num > 0 && shard_num <= total_shards

        begin
          ::RSpec::Core::ConfigurationOptions.new(rspec_args).configure(::RSpec.configuration)

          return if ::RSpec.world.wants_to_quit

          ::RSpec.configuration.load_spec_files
        ensure
          ::RSpec.world.announce_filters
        end

        return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code) if ::RSpec.world.wants_to_quit

        begin
          shards = ::RSpec::Sharder.build_shards(total_shards)
        rescue ::RSpec::Sharder::ShardError => e
          ::RSpec.configuration.error_stream.puts e.message
          return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code)
        end

        print_shards(shards)

        expected_total_duration = shards[shard_num - 1][:duration]

        shard_file_paths = shards[shard_num - 1][:file_paths]
        example_groups = ::RSpec.world.ordered_example_groups.select do |example_group|
          shard_file_paths.include?(example_group.metadata[:file_path])
        end
        example_count = ::RSpec.world.example_count(example_groups)

        new_durations = { }

        actual_total_duration = 0
        exit_code = ::RSpec.configuration.reporter.report(example_count) do |reporter|
          ::RSpec.configuration.with_suite_hooks do
            if example_count == 0 && ::RSpec.configuration.fail_if_no_examples
              return ::RSpec.configuration.failure_exit_code
            end

            group_results = example_groups.map do |example_group|
              result, duration = run_example_group(example_group, reporter)

              file_path = example_group.metadata[:file_path]
              actual_total_duration += duration
              new_durations[file_path] ||= 0
              new_durations[file_path] += duration

              result
            end

            success = group_results.all?
            exit_code = success ? 0 : 1
            if ::RSpec.world.non_example_failure
              success = false
              exit_code = ::RSpec.configuration.failure_exit_code
            end
            exit_code
          end
        end

        # Write results to .examples file.
        unless ::RSpec.configuration.dry_run
          persist_example_statuses(shard_file_paths)
        end

        if ::RSpec.configuration.dry_run
          if persist
            ::RSpec.configuration.output_stream.puts <<~EOF
              
              Dry run. Not saving to .rspec-sharder-durations.
            EOF
          end
        else
          if exit_code == 0
            # Print recorded durations and summary.
            ::RSpec.configuration.output_stream.puts 'Durations'

            new_durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
              ::RSpec.configuration.output_stream.puts "#{file_path},#{duration}"
            end

            ::RSpec.configuration.output_stream.puts <<~EOF
              
              Expected total duration: #{pretty_duration(expected_total_duration)}
              Actual total duration:   #{pretty_duration(actual_total_duration)}
              Diff:                    #{pretty_duration((actual_total_duration - expected_total_duration).abs)}
            EOF

            if persist
              # Write durations to .rspec-sharder-durations.
              ::RSpec.configuration.output_stream.puts <<~EOF

                Saving to .rspec-sharder-durations.
              EOF

              persist_durations(new_durations)
            end
          elsif persist
            ::RSpec.configuration.output_stream.puts <<~EOF
              
              RSpec failed. Not saving to .rspec-sharder-durations.
            EOF
          end
        end

        exit_code
      end

Private Class Methods

current_time_millis() click to toggle source
# File lib/rspec-sharder/runner.rb, line 170
def self.current_time_millis
  (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
end
persist_durations(durations) click to toggle source
# File lib/rspec-sharder/runner.rb, line 158
      def self.persist_durations(durations)
        File.open(".rspec-sharder-durations", "w+") do |file|
          file.puts <<~EOF
            # Generated by rspec-sharder on #{Time.now.to_s}. See `bundle exec rspec-sharder -h`.
            
          EOF
          durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
            file.puts "#{file_path},#{duration}"
          end
        end
      end
persist_example_statuses(file_paths) click to toggle source
# File lib/rspec-sharder/runner.rb, line 120
def self.persist_example_statuses(file_paths)
  return unless (path = ::RSpec.configuration.example_status_persistence_file_path)

  examples = ::RSpec.world.all_examples.select do |example|
    file_paths.include?(example.metadata[:file_path])
  end
  ::RSpec::Core::ExampleStatusPersister.persist(examples, path)
rescue SystemCallError => e
  ::RSpec.configuration.output_stream.puts "warning: failed to write results to #{path}"
end
pretty_duration(duration_millis) click to toggle source
# File lib/rspec-sharder/runner.rb, line 131
def self.pretty_duration(duration_millis)
  duration_seconds = (duration_millis / 1000.0).round
  minutes = duration_seconds / 60
  seconds = duration_seconds % 60

  minutes_str = "#{minutes} minute#{minutes == 1 ? '' : 's'}"
  seconds_str = "#{seconds} second#{seconds == 1 ? '' : 's'}"

  if minutes == 0
    seconds_str
  else
    "#{minutes_str}, #{seconds_str}"
  end
end
print_shards(shards) click to toggle source
run_example_group(example_group, reporter) click to toggle source
# File lib/rspec-sharder/runner.rb, line 174
def self.run_example_group(example_group, reporter)
  start_time = current_time_millis
  result = example_group.run(reporter)
  [result, (current_time_millis - start_time).to_i]
end