rspec-sharder 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/rspec-sharder/command.rb +2 -2
- data/lib/rspec-sharder/runner.rb +181 -0
- data/lib/rspec-sharder/sharder.rb +96 -0
- data/lib/rspec-sharder/version.rb +1 -1
- metadata +4 -3
- data/lib/rspec-sharder.rb +0 -270
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 456e3e2d41127ed5154b26a4dffaa32e6fabf159a9aaea4461630212a8209dee
|
4
|
+
data.tar.gz: 37d68db5f6317a270de6f96129842c5bf4e530cedfab973b03d53c16583a74b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ca04689abae34a1a1820ac1de65fb844377254251ba3e4f6420858873bed207140495846b23ea9a00c3ffa8c796747cca5023f7c463f2c5bb78f3fe760c1879
|
7
|
+
data.tar.gz: 43bbba87bf483eb6237a16c12c115ff1b1ef340b7fb527ec4a6d4e0c2bded1a8d794379e9b9cd6223040e7cb89c7e39d86014733198c0d7117d78233f38382b9
|
data/Gemfile.lock
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'optparse'
|
2
|
-
require 'rspec-sharder'
|
2
|
+
require 'rspec-sharder/runner'
|
3
3
|
|
4
4
|
def fail(message)
|
5
5
|
warn message
|
@@ -83,4 +83,4 @@ fail('fatal: invalid value for --total-shards') unless @total_shards > 0
|
|
83
83
|
fail('fatal: invalid value for --shard') unless @shard > 0
|
84
84
|
fail('fatal: --shard may not be greater than --total-shards') unless @shard <= @total_shards
|
85
85
|
|
86
|
-
exit RSpec::Sharder.run(total_shards: @total_shards, shard_num: @shard, persist: @persist, rspec_args: ARGV)
|
86
|
+
exit RSpec::Sharder::Runner.run(total_shards: @total_shards, shard_num: @shard, persist: @persist, rspec_args: ARGV)
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
require 'rspec-sharder/sharder'
|
3
|
+
|
4
|
+
module RSpec
|
5
|
+
module Sharder
|
6
|
+
module Runner
|
7
|
+
def self.run(total_shards:, shard_num:, persist:, rspec_args:)
|
8
|
+
raise "fatal: invalid total shards: #{total_shards}" unless total_shards.is_a?(Integer) && total_shards > 0
|
9
|
+
raise "fatal: invalid shard number: #{shard_num}" unless shard_num.is_a?(Integer) && shard_num > 0 && shard_num <= total_shards
|
10
|
+
|
11
|
+
begin
|
12
|
+
::RSpec::Core::ConfigurationOptions.new(rspec_args).configure(::RSpec.configuration)
|
13
|
+
|
14
|
+
return if ::RSpec.world.wants_to_quit
|
15
|
+
|
16
|
+
::RSpec.configuration.load_spec_files
|
17
|
+
ensure
|
18
|
+
::RSpec.world.announce_filters
|
19
|
+
end
|
20
|
+
|
21
|
+
return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code) if ::RSpec.world.wants_to_quit
|
22
|
+
|
23
|
+
begin
|
24
|
+
shards = ::RSpec::Sharder.build_shards(total_shards)
|
25
|
+
rescue ::RSpec::Sharder::ShardError => e
|
26
|
+
::RSpec.configuration.error_stream.puts e.message
|
27
|
+
return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code)
|
28
|
+
end
|
29
|
+
|
30
|
+
print_shards(shards)
|
31
|
+
|
32
|
+
expected_total_duration = shards[shard_num - 1][:duration]
|
33
|
+
|
34
|
+
shard_file_paths = shards[shard_num - 1][:file_paths]
|
35
|
+
example_groups = ::RSpec.world.ordered_example_groups.select do |example_group|
|
36
|
+
shard_file_paths.include?(example_group.metadata[:file_path])
|
37
|
+
end
|
38
|
+
example_count = ::RSpec.world.example_count(example_groups)
|
39
|
+
|
40
|
+
new_durations = { }
|
41
|
+
|
42
|
+
actual_total_duration = 0
|
43
|
+
exit_code = ::RSpec.configuration.reporter.report(example_count) do |reporter|
|
44
|
+
::RSpec.configuration.with_suite_hooks do
|
45
|
+
if example_count == 0 && ::RSpec.configuration.fail_if_no_examples
|
46
|
+
return ::RSpec.configuration.failure_exit_code
|
47
|
+
end
|
48
|
+
|
49
|
+
group_results = example_groups.map do |example_group|
|
50
|
+
result, duration = run_example_group(example_group, reporter)
|
51
|
+
|
52
|
+
file_path = example_group.metadata[:file_path]
|
53
|
+
actual_total_duration += duration
|
54
|
+
new_durations[file_path] ||= 0
|
55
|
+
new_durations[file_path] += duration
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
success = group_results.all?
|
61
|
+
exit_code = success ? 0 : 1
|
62
|
+
if ::RSpec.world.non_example_failure
|
63
|
+
success = false
|
64
|
+
exit_code = ::RSpec.configuration.failure_exit_code
|
65
|
+
end
|
66
|
+
exit_code
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Write results to .examples file.
|
71
|
+
unless ::RSpec.configuration.dry_run
|
72
|
+
persist_example_statuses(shard_file_paths)
|
73
|
+
end
|
74
|
+
|
75
|
+
if ::RSpec.configuration.dry_run
|
76
|
+
if persist
|
77
|
+
::RSpec.configuration.output_stream.puts <<~EOF
|
78
|
+
|
79
|
+
Dry run. Not saving to .rspec-sharder-durations.
|
80
|
+
EOF
|
81
|
+
end
|
82
|
+
else
|
83
|
+
if exit_code == 0
|
84
|
+
# Print recorded durations and summary.
|
85
|
+
::RSpec.configuration.output_stream.puts 'Durations'
|
86
|
+
|
87
|
+
new_durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
|
88
|
+
::RSpec.configuration.output_stream.puts "#{file_path},#{duration}"
|
89
|
+
end
|
90
|
+
|
91
|
+
::RSpec.configuration.output_stream.puts <<~EOF
|
92
|
+
|
93
|
+
Expected total duration: #{pretty_duration(expected_total_duration)}
|
94
|
+
Actual total duration: #{pretty_duration(actual_total_duration)}
|
95
|
+
Diff: #{pretty_duration((actual_total_duration - expected_total_duration).abs)}
|
96
|
+
EOF
|
97
|
+
|
98
|
+
if persist
|
99
|
+
# Write durations to .rspec-sharder-durations.
|
100
|
+
::RSpec.configuration.output_stream.puts <<~EOF
|
101
|
+
|
102
|
+
Saving to .rspec-sharder-durations.
|
103
|
+
EOF
|
104
|
+
|
105
|
+
persist_durations(new_durations)
|
106
|
+
end
|
107
|
+
elsif persist
|
108
|
+
::RSpec.configuration.output_stream.puts <<~EOF
|
109
|
+
|
110
|
+
RSpec failed. Not saving to .rspec-sharder-durations.
|
111
|
+
EOF
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
exit_code
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def self.persist_example_statuses(file_paths)
|
121
|
+
return unless (path = ::RSpec.configuration.example_status_persistence_file_path)
|
122
|
+
|
123
|
+
examples = ::RSpec.world.all_examples.select do |example|
|
124
|
+
file_paths.include?(example.metadata[:file_path])
|
125
|
+
end
|
126
|
+
::RSpec::Core::ExampleStatusPersister.persist(examples, path)
|
127
|
+
rescue SystemCallError => e
|
128
|
+
::RSpec.configuration.output_stream.puts "warning: failed to write results to #{path}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.pretty_duration(duration_millis)
|
132
|
+
duration_seconds = (duration_millis / 1000.0).round
|
133
|
+
minutes = duration_seconds / 60
|
134
|
+
seconds = duration_seconds % 60
|
135
|
+
|
136
|
+
minutes_str = "#{minutes} minute#{minutes == 1 ? '' : 's'}"
|
137
|
+
seconds_str = "#{seconds} second#{seconds == 1 ? '' : 's'}"
|
138
|
+
|
139
|
+
if minutes == 0
|
140
|
+
seconds_str
|
141
|
+
else
|
142
|
+
"#{minutes_str}, #{seconds_str}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.print_shards(shards)
|
147
|
+
shards.each_with_index do |shard, i|
|
148
|
+
::RSpec.configuration.output_stream.puts(
|
149
|
+
"Shard #{i + 1} (Files: #{shard[:file_paths].size}, Duration: #{pretty_duration(shard[:duration])}):"
|
150
|
+
)
|
151
|
+
shard[:file_paths].each do |file_path|
|
152
|
+
::RSpec.configuration.output_stream.puts file_path
|
153
|
+
end
|
154
|
+
::RSpec.configuration.output_stream.puts unless i == shards.size - 1
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.persist_durations(durations)
|
159
|
+
File.open(".rspec-sharder-durations", "w+") do |file|
|
160
|
+
file.puts <<~EOF
|
161
|
+
# Generated by rspec-sharder on #{Time.now.to_s}. See `bundle exec rspec-sharder -h`.
|
162
|
+
|
163
|
+
EOF
|
164
|
+
durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
|
165
|
+
file.puts "#{file_path},#{duration}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.current_time_millis
|
171
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.run_example_group(example_group, reporter)
|
175
|
+
start_time = current_time_millis
|
176
|
+
result = example_group.run(reporter)
|
177
|
+
[result, (current_time_millis - start_time).to_i]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Sharder
|
5
|
+
class ShardError < StandardError; end
|
6
|
+
|
7
|
+
def self.load_recorded_durations
|
8
|
+
durations = { }
|
9
|
+
|
10
|
+
missing_files = 0
|
11
|
+
|
12
|
+
if File.exist?('.rspec-sharder-durations')
|
13
|
+
File.readlines('.rspec-sharder-durations').each_with_index do |line, index|
|
14
|
+
line = line.strip
|
15
|
+
|
16
|
+
if !line.start_with?('#') && !line.empty?
|
17
|
+
parts = line.split(',')
|
18
|
+
|
19
|
+
unless parts.length == 2
|
20
|
+
raise ShardError.new("fatal: invalid .rspec-sharder-durations at line #{index + 1}")
|
21
|
+
end
|
22
|
+
|
23
|
+
file_path = parts[0].strip
|
24
|
+
|
25
|
+
if file_path.empty?
|
26
|
+
raise ShardError.new("fatal: invalid file path in .rspec-sharder-durations at line #{index + 1}")
|
27
|
+
end
|
28
|
+
|
29
|
+
unless File.exist?(file_path)
|
30
|
+
missing_files += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
duration = Integer(parts[1])
|
35
|
+
rescue ArgumentError => e
|
36
|
+
raise ShardError.new("fatal: invalid .rspec-sharder-durations at line #{index + 1}")
|
37
|
+
end
|
38
|
+
|
39
|
+
durations[file_path] = duration
|
40
|
+
end
|
41
|
+
end.compact
|
42
|
+
|
43
|
+
if missing_files > 0
|
44
|
+
::RSpec.configuration.output_stream.puts <<~EOF
|
45
|
+
warning: #{missing_files} file(s) in .rspec-sharder-durations do not exist, consider regenerating
|
46
|
+
|
47
|
+
EOF
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
durations
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.build_shards(total_shards)
|
55
|
+
durations = load_recorded_durations
|
56
|
+
|
57
|
+
files = { }
|
58
|
+
|
59
|
+
missing_files = 0
|
60
|
+
::RSpec.world.ordered_example_groups.each do |example_group|
|
61
|
+
file_path = example_group.metadata[:file_path]
|
62
|
+
files[file_path] ||= 0
|
63
|
+
if durations[file_path]
|
64
|
+
files[file_path] = durations[file_path]
|
65
|
+
else
|
66
|
+
missing_files += 1
|
67
|
+
# Assume 1000 milliseconds per example.
|
68
|
+
files[file_path] += ::RSpec.world.example_count([example_group]) * 1000
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if missing_files > 0
|
73
|
+
::RSpec.configuration.output_stream.puts <<~EOF
|
74
|
+
warning: #{missing_files} file(s) in not found in .rspec-sharder-durations, consider regenerating
|
75
|
+
|
76
|
+
EOF
|
77
|
+
end
|
78
|
+
|
79
|
+
shards = (1..total_shards).map { { duration: 0, file_paths: [] } }
|
80
|
+
|
81
|
+
# First sort by duration to ensure large files are distributed evenly.
|
82
|
+
# Next, sort by path to ensure shards are generated deterministically.
|
83
|
+
# Note that files is a map, sorting it turns it into an array of arrays.
|
84
|
+
files = files.sort_by { |file_path, duration| [duration, file_path] }.reverse
|
85
|
+
files.each do |file_path, duration|
|
86
|
+
shards.sort_by! { |shard| shard[:duration] }
|
87
|
+
shards[0][:file_paths] << file_path
|
88
|
+
shards[0][:duration] += duration
|
89
|
+
end
|
90
|
+
|
91
|
+
shards.each { |shard| shard[:file_paths].sort! }
|
92
|
+
|
93
|
+
shards
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-sharder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Dower
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec-core
|
@@ -38,8 +38,9 @@ files:
|
|
38
38
|
- LICENSE
|
39
39
|
- README.md
|
40
40
|
- bin/rspec-sharder
|
41
|
-
- lib/rspec-sharder.rb
|
42
41
|
- lib/rspec-sharder/command.rb
|
42
|
+
- lib/rspec-sharder/runner.rb
|
43
|
+
- lib/rspec-sharder/sharder.rb
|
43
44
|
- lib/rspec-sharder/version.rb
|
44
45
|
- rspec-sharder.gemspec
|
45
46
|
- scripts/release.sh
|
data/lib/rspec-sharder.rb
DELETED
@@ -1,270 +0,0 @@
|
|
1
|
-
require 'rspec/core'
|
2
|
-
|
3
|
-
module RSpec
|
4
|
-
module Sharder
|
5
|
-
|
6
|
-
class ShardError < StandardError; end
|
7
|
-
|
8
|
-
def self.run(total_shards:, shard_num:, persist:, rspec_args:)
|
9
|
-
raise "fatal: invalid total shards: #{total_shards}" unless total_shards.is_a?(Integer) && total_shards > 0
|
10
|
-
raise "fatal: invalid shard number: #{shard_num}" unless shard_num.is_a?(Integer) && shard_num > 0 && shard_num <= total_shards
|
11
|
-
|
12
|
-
begin
|
13
|
-
::RSpec::Core::ConfigurationOptions.new(rspec_args).configure(::RSpec.configuration)
|
14
|
-
|
15
|
-
return if ::RSpec.world.wants_to_quit
|
16
|
-
|
17
|
-
::RSpec.configuration.load_spec_files
|
18
|
-
ensure
|
19
|
-
::RSpec.world.announce_filters
|
20
|
-
end
|
21
|
-
|
22
|
-
return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code) if ::RSpec.world.wants_to_quit
|
23
|
-
|
24
|
-
all_durations = load_recorded_durations
|
25
|
-
|
26
|
-
begin
|
27
|
-
shards = build_shards(total_shards, shard_num, all_durations)
|
28
|
-
rescue ShardError => e
|
29
|
-
::RSpec.configuration.error_stream.puts e.message
|
30
|
-
return ::RSpec.configuration.reporter.exit_early(::RSpec.configuration.failure_exit_code)
|
31
|
-
end
|
32
|
-
|
33
|
-
print_shards(shards)
|
34
|
-
|
35
|
-
expected_total_duration = shards[shard_num - 1][:duration]
|
36
|
-
|
37
|
-
shard_file_paths = shards[shard_num - 1][:file_paths]
|
38
|
-
example_groups = ::RSpec.world.ordered_example_groups.select do |example_group|
|
39
|
-
shard_file_paths.include?(example_group.metadata[:file_path])
|
40
|
-
end
|
41
|
-
example_count = ::RSpec.world.example_count(example_groups)
|
42
|
-
|
43
|
-
new_durations = { }
|
44
|
-
|
45
|
-
actual_total_duration = 0
|
46
|
-
exit_code = ::RSpec.configuration.reporter.report(example_count) do |reporter|
|
47
|
-
::RSpec.configuration.with_suite_hooks do
|
48
|
-
if example_count == 0 && ::RSpec.configuration.fail_if_no_examples
|
49
|
-
return ::RSpec.configuration.failure_exit_code
|
50
|
-
end
|
51
|
-
|
52
|
-
group_results = example_groups.map do |example_group|
|
53
|
-
result, duration = run_example_group(example_group, reporter)
|
54
|
-
|
55
|
-
file_path = example_group.metadata[:file_path]
|
56
|
-
actual_total_duration += duration
|
57
|
-
new_durations[file_path] ||= 0
|
58
|
-
new_durations[file_path] += duration
|
59
|
-
|
60
|
-
result
|
61
|
-
end
|
62
|
-
|
63
|
-
success = group_results.all?
|
64
|
-
exit_code = success ? 0 : 1
|
65
|
-
if ::RSpec.world.non_example_failure
|
66
|
-
success = false
|
67
|
-
exit_code = ::RSpec.configuration.failure_exit_code
|
68
|
-
end
|
69
|
-
exit_code
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# Write results to .examples file.
|
74
|
-
unless ::RSpec.configuration.dry_run
|
75
|
-
persist_example_statuses(shard_file_paths)
|
76
|
-
end
|
77
|
-
|
78
|
-
if ::RSpec.configuration.dry_run
|
79
|
-
if persist
|
80
|
-
::RSpec.configuration.output_stream.puts <<~EOF
|
81
|
-
|
82
|
-
Dry run. Not saving to .rspec-sharder-durations.
|
83
|
-
EOF
|
84
|
-
end
|
85
|
-
else
|
86
|
-
if exit_code == 0
|
87
|
-
# Print recorded durations and summary.
|
88
|
-
::RSpec.configuration.output_stream.puts 'Durations'
|
89
|
-
|
90
|
-
new_durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
|
91
|
-
::RSpec.configuration.output_stream.puts "#{file_path},#{duration}"
|
92
|
-
end
|
93
|
-
|
94
|
-
::RSpec.configuration.output_stream.puts <<~EOF
|
95
|
-
|
96
|
-
Expected total duration: #{pretty_duration(expected_total_duration)}
|
97
|
-
Actual total duration: #{pretty_duration(actual_total_duration)}
|
98
|
-
Diff: #{pretty_duration((actual_total_duration - expected_total_duration).abs)}
|
99
|
-
EOF
|
100
|
-
|
101
|
-
if persist
|
102
|
-
# Write durations to .rspec-sharder-durations.
|
103
|
-
::RSpec.configuration.output_stream.puts <<~EOF
|
104
|
-
|
105
|
-
Saving to .rspec-sharder-durations.
|
106
|
-
EOF
|
107
|
-
|
108
|
-
persist_durations(new_durations)
|
109
|
-
end
|
110
|
-
elsif persist
|
111
|
-
::RSpec.configuration.output_stream.puts <<~EOF
|
112
|
-
|
113
|
-
RSpec failed. Not saving to .rspec-sharder-durations.
|
114
|
-
EOF
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
exit_code
|
119
|
-
end
|
120
|
-
|
121
|
-
private
|
122
|
-
|
123
|
-
def self.load_recorded_durations
|
124
|
-
durations = { }
|
125
|
-
|
126
|
-
missing_files = 0
|
127
|
-
|
128
|
-
if File.exist?('.rspec-sharder-durations')
|
129
|
-
File.readlines('.rspec-sharder-durations').each_with_index do |line, index|
|
130
|
-
line = line.strip
|
131
|
-
|
132
|
-
if !line.start_with?('#') && !line.empty?
|
133
|
-
parts = line.split(',')
|
134
|
-
|
135
|
-
unless parts.length == 2
|
136
|
-
raise ShardError.new("fatal: invalid .rspec-sharder-durations at line #{index + 1}")
|
137
|
-
end
|
138
|
-
|
139
|
-
file_path = parts[0].strip
|
140
|
-
|
141
|
-
if file_path.empty?
|
142
|
-
raise ShardError.new("fatal: invalid file path in .rspec-sharder-durations at line #{index + 1}")
|
143
|
-
end
|
144
|
-
|
145
|
-
unless File.exist?(file_path)
|
146
|
-
missing_files += 1
|
147
|
-
end
|
148
|
-
|
149
|
-
begin
|
150
|
-
duration = Integer(parts[1])
|
151
|
-
rescue ArgumentError => e
|
152
|
-
raise ShardError.new("fatal: invalid .rspec-sharder-durations at line #{index + 1}")
|
153
|
-
end
|
154
|
-
|
155
|
-
durations[file_path] = duration
|
156
|
-
end
|
157
|
-
end.compact
|
158
|
-
|
159
|
-
if missing_files > 0
|
160
|
-
::RSpec.configuration.output_stream.puts <<~EOF
|
161
|
-
warning: #{missing_files} file(s) in .rspec-sharder-durations do not exist, consider regenerating
|
162
|
-
|
163
|
-
EOF
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
durations
|
168
|
-
end
|
169
|
-
|
170
|
-
def self.build_shards(total_shards, shard_num, durations)
|
171
|
-
files = { }
|
172
|
-
|
173
|
-
missing_files = 0
|
174
|
-
::RSpec.world.ordered_example_groups.each do |example_group|
|
175
|
-
file_path = example_group.metadata[:file_path]
|
176
|
-
files[file_path] ||= 0
|
177
|
-
if durations[file_path]
|
178
|
-
files[file_path] = durations[file_path]
|
179
|
-
else
|
180
|
-
missing_files += 1
|
181
|
-
# Assume 1000 milliseconds per example.
|
182
|
-
files[file_path] += ::RSpec.world.example_count([example_group]) * 1000
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
if missing_files > 0
|
187
|
-
::RSpec.configuration.output_stream.puts <<~EOF
|
188
|
-
warning: #{missing_files} file(s) in not found in .rspec-sharder-durations, consider regenerating
|
189
|
-
|
190
|
-
EOF
|
191
|
-
end
|
192
|
-
|
193
|
-
shards = (1..total_shards).map { { duration: 0, file_paths: [] } }
|
194
|
-
|
195
|
-
# First sort by duration to ensure large files are distributed evenly.
|
196
|
-
# Next, sort by path to ensure shards are generated deterministically.
|
197
|
-
# Note that files is a map, sorting it turns it into an array of arrays.
|
198
|
-
files = files.sort_by { |file_path, duration| [duration, file_path] }.reverse
|
199
|
-
files.each do |file_path, duration|
|
200
|
-
shards.sort_by! { |shard| shard[:duration] }
|
201
|
-
shards[0][:file_paths] << file_path
|
202
|
-
shards[0][:duration] += duration
|
203
|
-
end
|
204
|
-
|
205
|
-
shards.each { |shard| shard[:file_paths].sort! }
|
206
|
-
|
207
|
-
shards
|
208
|
-
end
|
209
|
-
|
210
|
-
def self.persist_example_statuses(file_paths)
|
211
|
-
return unless (path = ::RSpec.configuration.example_status_persistence_file_path)
|
212
|
-
|
213
|
-
examples = ::RSpec.world.all_examples.select do |example|
|
214
|
-
file_paths.include?(example.metadata[:file_path])
|
215
|
-
end
|
216
|
-
::RSpec::Core::ExampleStatusPersister.persist(examples, path)
|
217
|
-
rescue SystemCallError => e
|
218
|
-
::RSpec.configuration.output_stream.puts "warning: failed to write results to #{path}"
|
219
|
-
end
|
220
|
-
|
221
|
-
def self.pretty_duration(duration_millis)
|
222
|
-
duration_seconds = (duration_millis / 1000.0).round
|
223
|
-
minutes = duration_seconds / 60
|
224
|
-
seconds = duration_seconds % 60
|
225
|
-
|
226
|
-
minutes_str = "#{minutes} minute#{minutes == 1 ? '' : 's'}"
|
227
|
-
seconds_str = "#{seconds} second#{seconds == 1 ? '' : 's'}"
|
228
|
-
|
229
|
-
if minutes == 0
|
230
|
-
seconds_str
|
231
|
-
else
|
232
|
-
"#{minutes_str}, #{seconds_str}"
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def self.print_shards(shards)
|
237
|
-
shards.each_with_index do |shard, i|
|
238
|
-
::RSpec.configuration.output_stream.puts(
|
239
|
-
"Shard #{i + 1} (Files: #{shard[:file_paths].size}, Duration: #{pretty_duration(shard[:duration])}):"
|
240
|
-
)
|
241
|
-
shard[:file_paths].each do |file_path|
|
242
|
-
::RSpec.configuration.output_stream.puts file_path
|
243
|
-
end
|
244
|
-
::RSpec.configuration.output_stream.puts unless i == shards.size - 1
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def self.persist_durations(durations)
|
249
|
-
File.open(".rspec-sharder-durations", "w+") do |file|
|
250
|
-
file.puts <<~EOF
|
251
|
-
# Generated by rspec-sharder on #{Time.now.to_s}. See `bundle exec rspec-sharder -h`.
|
252
|
-
|
253
|
-
EOF
|
254
|
-
durations.sort_by { |file_path, duration| file_path }.each do |file_path, duration|
|
255
|
-
file.puts "#{file_path},#{duration}"
|
256
|
-
end
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def self.current_time_millis
|
261
|
-
(Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
|
262
|
-
end
|
263
|
-
|
264
|
-
def self.run_example_group(example_group, reporter)
|
265
|
-
start_time = current_time_millis
|
266
|
-
result = example_group.run(reporter)
|
267
|
-
[result, (current_time_millis - start_time).to_i]
|
268
|
-
end
|
269
|
-
end
|
270
|
-
end
|