module RSpec::Sharder

Constants

VERSION

Public Class Methods

build_shards(total_shards) click to toggle source
# File lib/rspec-sharder/sharder.rb, line 54
    def self.build_shards(total_shards)
      durations = load_recorded_durations

      files = { }

      missing_files = 0
      ::RSpec.world.ordered_example_groups.each do |example_group|
        file_path = example_group.metadata[:file_path]
        files[file_path] ||= 0
        if durations[file_path]
          files[file_path] = durations[file_path]
        else
          missing_files += 1
          # Assume 1000 milliseconds per example.
          files[file_path] += ::RSpec.world.example_count([example_group]) * 1000
        end
      end

      if missing_files > 0
        ::RSpec.configuration.output_stream.puts <<~EOF
          warning: #{missing_files} file(s) in not found in .rspec-sharder-durations, consider regenerating

        EOF
      end

      shards = (1..total_shards).map { { duration: 0, file_paths: [] } }

      # First sort by duration to ensure large files are distributed evenly.
      # Next, sort by path to ensure shards are generated deterministically.
      # Note that files is a map, sorting it turns it into an array of arrays.
      files = files.sort_by { |file_path, duration| [duration, file_path] }.reverse
      files.each do |file_path, duration|
        shards.sort_by! { |shard| shard[:duration] }
        shards[0][:file_paths] << file_path
        shards[0][:duration] += duration
      end

      shards.each { |shard| shard[:file_paths].sort! }

      shards
    end
load_recorded_durations() click to toggle source
# File lib/rspec-sharder/sharder.rb, line 7
    def self.load_recorded_durations
      durations = { }

      missing_files = 0

      if File.exist?('.rspec-sharder-durations')
        File.readlines('.rspec-sharder-durations').each_with_index do |line, index|
          line = line.strip

          if !line.start_with?('#') && !line.empty?
            parts = line.split(',')

            unless parts.length == 2
              raise ShardError.new("fatal: invalid .rspec-sharder-durations at line #{index + 1}")
            end

            file_path = parts[0].strip

            if file_path.empty?
              raise ShardError.new("fatal: invalid file path in .rspec-sharder-durations at line #{index + 1}")
            end

            unless File.exist?(file_path)
              missing_files += 1
            end

            begin
              duration = Integer(parts[1])
            rescue ArgumentError => e
              raise ShardError.new("fatal: invalid .rspec-sharder-durations at line #{index + 1}")
            end

            durations[file_path] = duration
          end
        end.compact

        if missing_files > 0
          ::RSpec.configuration.output_stream.puts <<~EOF
            warning: #{missing_files} file(s) in .rspec-sharder-durations do not exist, consider regenerating

          EOF
        end
      end

      durations
    end