Module: RSpec::Sharder

Defined in:
lib/rspec-sharder/runner.rb,
lib/rspec-sharder/sharder.rb,
lib/rspec-sharder/version.rb

Defined Under Namespace

Modules: Runner Classes: ShardError

Constant Summary collapse

VERSION =
"0.0.5"

Class Method Summary collapse

Class Method Details

.build_shards(total_shards) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 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.[: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_durationsObject



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 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