parallel_tests 3.3.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +53 -27
- data/bin/parallel_cucumber +2 -1
- data/bin/parallel_rspec +2 -1
- data/bin/parallel_spinach +2 -1
- data/bin/parallel_test +2 -1
- data/lib/parallel_tests/cli.rb +154 -92
- data/lib/parallel_tests/cucumber/failures_logger.rb +1 -1
- data/lib/parallel_tests/cucumber/features_with_steps.rb +4 -3
- data/lib/parallel_tests/cucumber/runner.rb +10 -7
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +4 -4
- data/lib/parallel_tests/cucumber/scenarios.rb +9 -8
- data/lib/parallel_tests/gherkin/io.rb +2 -3
- data/lib/parallel_tests/gherkin/listener.rb +9 -10
- data/lib/parallel_tests/gherkin/runner.rb +29 -35
- data/lib/parallel_tests/gherkin/runtime_logger.rb +2 -1
- data/lib/parallel_tests/grouper.rb +57 -6
- data/lib/parallel_tests/pids.rb +5 -4
- data/lib/parallel_tests/railtie.rb +1 -0
- data/lib/parallel_tests/rspec/failures_logger.rb +2 -2
- data/lib/parallel_tests/rspec/logger_base.rb +9 -7
- data/lib/parallel_tests/rspec/runner.rb +32 -19
- data/lib/parallel_tests/rspec/runtime_logger.rb +12 -10
- data/lib/parallel_tests/rspec/summary_logger.rb +2 -3
- data/lib/parallel_tests/spinach/runner.rb +6 -2
- data/lib/parallel_tests/tasks.rb +130 -71
- data/lib/parallel_tests/test/runner.rb +90 -41
- data/lib/parallel_tests/test/runtime_logger.rb +19 -14
- data/lib/parallel_tests/version.rb +2 -1
- data/lib/parallel_tests.rb +13 -13
- metadata +10 -10
@@ -1,8 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'shellwords'
|
1
3
|
require 'parallel_tests'
|
2
4
|
|
3
5
|
module ParallelTests
|
4
6
|
module Test
|
5
7
|
class Runner
|
8
|
+
RuntimeLogTooSmallError = Class.new(StandardError)
|
9
|
+
|
6
10
|
class << self
|
7
11
|
# --- usually overwritten by other runners
|
8
12
|
|
@@ -14,13 +18,24 @@ module ParallelTests
|
|
14
18
|
/_(test|spec).rb$/
|
15
19
|
end
|
16
20
|
|
21
|
+
def default_test_folder
|
22
|
+
"test"
|
23
|
+
end
|
24
|
+
|
17
25
|
def test_file_name
|
18
26
|
"test"
|
19
27
|
end
|
20
28
|
|
21
29
|
def run_tests(test_files, process_number, num_processes, options)
|
22
|
-
require_list = test_files.map { |file| file.
|
23
|
-
cmd =
|
30
|
+
require_list = test_files.map { |file| file.gsub(" ", "\\ ") }.join(" ")
|
31
|
+
cmd = [
|
32
|
+
*executable,
|
33
|
+
'-Itest',
|
34
|
+
'-e',
|
35
|
+
"%w[#{require_list}].each { |f| require %{./\#{f}} }",
|
36
|
+
'--',
|
37
|
+
*options[:test_options]
|
38
|
+
]
|
24
39
|
execute_command(cmd, process_number, num_processes, options)
|
25
40
|
end
|
26
41
|
|
@@ -32,7 +47,7 @@ module ParallelTests
|
|
32
47
|
# --- usually used by other runners
|
33
48
|
|
34
49
|
# finds all tests and partitions them into groups
|
35
|
-
def tests_in_groups(tests, num_groups, options={})
|
50
|
+
def tests_in_groups(tests, num_groups, options = {})
|
36
51
|
tests = tests_with_size(tests, options)
|
37
52
|
Grouper.in_even_groups_by_size(tests, num_groups, options)
|
38
53
|
end
|
@@ -46,12 +61,19 @@ module ParallelTests
|
|
46
61
|
when :filesize
|
47
62
|
sort_by_filesize(tests)
|
48
63
|
when :runtime
|
49
|
-
sort_by_runtime(
|
64
|
+
sort_by_runtime(
|
65
|
+
tests, runtimes(tests, options),
|
66
|
+
options.merge(allowed_missing: (options[:allowed_missing_percent] || 50) / 100.0)
|
67
|
+
)
|
50
68
|
when nil
|
51
69
|
# use recorded test runtime if we got enough data
|
52
|
-
runtimes =
|
70
|
+
runtimes = begin
|
71
|
+
runtimes(tests, options)
|
72
|
+
rescue StandardError
|
73
|
+
[]
|
74
|
+
end
|
53
75
|
if runtimes.size * 1.5 > tests.size
|
54
|
-
puts "Using recorded test runtime"
|
76
|
+
puts "Using recorded test runtime" unless options[:quiet]
|
55
77
|
sort_by_runtime(tests, runtimes)
|
56
78
|
else
|
57
79
|
sort_by_filesize(tests)
|
@@ -64,35 +86,44 @@ module ParallelTests
|
|
64
86
|
end
|
65
87
|
|
66
88
|
def execute_command(cmd, process_number, num_processes, options)
|
89
|
+
number = test_env_number(process_number, options).to_s
|
67
90
|
env = (options[:env] || {}).merge(
|
68
|
-
"TEST_ENV_NUMBER" =>
|
91
|
+
"TEST_ENV_NUMBER" => number,
|
69
92
|
"PARALLEL_TEST_GROUPS" => num_processes.to_s,
|
70
|
-
"PARALLEL_PID_FILE" => ParallelTests.pid_file_path
|
93
|
+
"PARALLEL_PID_FILE" => ParallelTests.pid_file_path
|
71
94
|
)
|
72
|
-
cmd = "nice
|
73
|
-
cmd = "#{cmd} 2>&1" if options[:combine_stderr]
|
95
|
+
cmd = ["nice", *cmd] if options[:nice]
|
74
96
|
|
75
|
-
|
97
|
+
# being able to run with for example `-output foo-$TEST_ENV_NUMBER` worked originally and is convenient
|
98
|
+
cmd.map! { |c| c.gsub("$TEST_ENV_NUMBER", number).gsub("${TEST_ENV_NUMBER}", number) }
|
99
|
+
|
100
|
+
print_command(cmd, env) if report_process_command?(options) && !options[:serialize_stdout]
|
76
101
|
|
77
102
|
execute_command_and_capture_output(env, cmd, options)
|
78
103
|
end
|
79
104
|
|
105
|
+
def print_command(command, env)
|
106
|
+
env_str = ['TEST_ENV_NUMBER', 'PARALLEL_TEST_GROUPS'].map { |e| "#{e}=#{env[e]}" }.join(' ')
|
107
|
+
puts [env_str, Shellwords.shelljoin(command)].compact.join(' ')
|
108
|
+
end
|
109
|
+
|
80
110
|
def execute_command_and_capture_output(env, cmd, options)
|
111
|
+
popen_options = {} # do not add `pgroup: true`, it will break `binding.irb` inside the test
|
112
|
+
popen_options[:err] = [:child, :out] if options[:combine_stderr]
|
113
|
+
|
81
114
|
pid = nil
|
82
|
-
output = IO.popen(env, cmd) do |io|
|
115
|
+
output = IO.popen(env, cmd, popen_options) do |io|
|
83
116
|
pid = io.pid
|
84
117
|
ParallelTests.pids.add(pid)
|
85
118
|
capture_output(io, env, options)
|
86
119
|
end
|
87
120
|
ParallelTests.pids.delete(pid) if pid
|
88
121
|
exitstatus = $?.exitstatus
|
89
|
-
seed = output[/seed (\d+)/,1]
|
122
|
+
seed = output[/seed (\d+)/, 1]
|
90
123
|
|
91
|
-
if report_process_command?(options) && options[:serialize_stdout]
|
92
|
-
output = [cmd, output].join("\n")
|
93
|
-
end
|
124
|
+
output = "#{Shellwords.shelljoin(cmd)}\n#{output}" if report_process_command?(options) && options[:serialize_stdout]
|
94
125
|
|
95
|
-
{:stdout
|
126
|
+
{ env: env, stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
|
96
127
|
end
|
97
128
|
|
98
129
|
def find_results(test_output)
|
@@ -104,7 +135,7 @@ module ParallelTests
|
|
104
135
|
end.compact
|
105
136
|
end
|
106
137
|
|
107
|
-
def test_env_number(process_number, options={})
|
138
|
+
def test_env_number(process_number, options = {})
|
108
139
|
if process_number == 0 && !options[:first_is_1]
|
109
140
|
''
|
110
141
|
else
|
@@ -114,39 +145,42 @@ module ParallelTests
|
|
114
145
|
|
115
146
|
def summarize_results(results)
|
116
147
|
sums = sum_up_results(results)
|
117
|
-
sums.sort.map{|word, number|
|
148
|
+
sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
118
149
|
end
|
119
150
|
|
120
151
|
# remove old seed and add new seed
|
121
152
|
def command_with_seed(cmd, seed)
|
122
|
-
clean = cmd
|
123
|
-
|
153
|
+
clean = remove_command_arguments(cmd, '--seed')
|
154
|
+
[*clean, '--seed', seed]
|
124
155
|
end
|
125
156
|
|
126
157
|
protected
|
127
158
|
|
128
159
|
def executable
|
129
|
-
ENV['PARALLEL_TESTS_EXECUTABLE']
|
160
|
+
if (executable = ENV['PARALLEL_TESTS_EXECUTABLE'])
|
161
|
+
[executable]
|
162
|
+
else
|
163
|
+
determine_executable
|
164
|
+
end
|
130
165
|
end
|
131
166
|
|
132
167
|
def determine_executable
|
133
|
-
"ruby"
|
168
|
+
["ruby"]
|
134
169
|
end
|
135
170
|
|
136
171
|
def sum_up_results(results)
|
137
|
-
results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
|
172
|
+
results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
|
138
173
|
counts = results.scan(/(\d+) (\w+)/)
|
139
|
-
counts.
|
174
|
+
counts.each_with_object(Hash.new(0)) do |(number, word), sum|
|
140
175
|
sum[word] += number.to_i
|
141
|
-
sum
|
142
176
|
end
|
143
177
|
end
|
144
178
|
|
145
179
|
# read output of the process and print it in chunks
|
146
|
-
def capture_output(out, env, options={})
|
147
|
-
result = ""
|
148
|
-
|
149
|
-
|
180
|
+
def capture_output(out, env, options = {})
|
181
|
+
result = +""
|
182
|
+
begin
|
183
|
+
loop do
|
150
184
|
read = out.readpartial(1000000) # read whatever chunk we can get
|
151
185
|
if Encoding.default_internal
|
152
186
|
read = read.force_encoding(Encoding.default_internal)
|
@@ -159,11 +193,13 @@ module ParallelTests
|
|
159
193
|
$stdout.flush
|
160
194
|
end
|
161
195
|
end
|
162
|
-
|
196
|
+
rescue EOFError
|
197
|
+
nil
|
198
|
+
end
|
163
199
|
result
|
164
200
|
end
|
165
201
|
|
166
|
-
def sort_by_runtime(tests, runtimes, options={})
|
202
|
+
def sort_by_runtime(tests, runtimes, options = {})
|
167
203
|
allowed_missing = options[:allowed_missing] || 1.0
|
168
204
|
allowed_missing = tests.size * allowed_missing
|
169
205
|
|
@@ -173,14 +209,12 @@ module ParallelTests
|
|
173
209
|
allowed_missing -= 1 unless time = runtimes[test]
|
174
210
|
if allowed_missing < 0
|
175
211
|
log = options[:runtime_log] || runtime_log
|
176
|
-
raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
|
212
|
+
raise RuntimeLogTooSmallError, "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
|
177
213
|
end
|
178
214
|
[test, time]
|
179
215
|
end
|
180
216
|
|
181
|
-
if options[:verbose]
|
182
|
-
puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
|
183
|
-
end
|
217
|
+
puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests" if options[:verbose]
|
184
218
|
|
185
219
|
set_unknown_runtime tests, options
|
186
220
|
end
|
@@ -190,7 +224,7 @@ module ParallelTests
|
|
190
224
|
lines = File.read(log).split("\n")
|
191
225
|
lines.each_with_object({}) do |line, times|
|
192
226
|
test, _, time = line.rpartition(':')
|
193
|
-
next unless test
|
227
|
+
next unless test && time
|
194
228
|
times[test] = time.to_f if tests.include?(test)
|
195
229
|
end
|
196
230
|
end
|
@@ -217,7 +251,7 @@ module ParallelTests
|
|
217
251
|
end.uniq
|
218
252
|
end
|
219
253
|
|
220
|
-
def files_in_folder(folder, options={})
|
254
|
+
def files_in_folder(folder, options = {})
|
221
255
|
pattern = if options[:symlinks] == false # not nil or true
|
222
256
|
"**/*"
|
223
257
|
else
|
@@ -225,7 +259,22 @@ module ParallelTests
|
|
225
259
|
# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
|
226
260
|
"**{,/*/**}/*"
|
227
261
|
end
|
228
|
-
Dir[File.join(folder, pattern)].uniq
|
262
|
+
Dir[File.join(folder, pattern)].uniq.sort
|
263
|
+
end
|
264
|
+
|
265
|
+
def remove_command_arguments(command, *args)
|
266
|
+
remove_next = false
|
267
|
+
command.select do |arg|
|
268
|
+
if remove_next
|
269
|
+
remove_next = false
|
270
|
+
false
|
271
|
+
elsif args.include?(arg)
|
272
|
+
remove_next = true
|
273
|
+
false
|
274
|
+
else
|
275
|
+
true
|
276
|
+
end
|
277
|
+
end
|
229
278
|
end
|
230
279
|
|
231
280
|
private
|
@@ -236,12 +285,12 @@ module ParallelTests
|
|
236
285
|
known, unknown = tests.partition(&:last)
|
237
286
|
return if unknown.empty?
|
238
287
|
unknown_runtime = options[:unknown_runtime] ||
|
239
|
-
(known.empty? ? 1 : known.map!(&:last).
|
288
|
+
(known.empty? ? 1 : known.map!(&:last).sum / known.size) # average
|
240
289
|
unknown.each { |set| set[1] = unknown_runtime }
|
241
290
|
end
|
242
291
|
|
243
292
|
def report_process_command?(options)
|
244
|
-
options[:verbose] || options[:
|
293
|
+
options[:verbose] || options[:verbose_command]
|
245
294
|
end
|
246
295
|
end
|
247
296
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'parallel_tests'
|
2
3
|
require 'parallel_tests/test/runner'
|
3
4
|
|
@@ -22,7 +23,7 @@ module ParallelTests
|
|
22
23
|
separator = "\n"
|
23
24
|
groups = logfile.read.split(separator).map { |line| line.split(":") }.group_by(&:first)
|
24
25
|
lines = groups.map do |file, times|
|
25
|
-
time = "%.2f" % times.map(&:last).map(&:to_f).
|
26
|
+
time = "%.2f" % times.map(&:last).map(&:to_f).sum
|
26
27
|
"#{file}:#{time}"
|
27
28
|
end
|
28
29
|
logfile.rewind
|
@@ -34,7 +35,7 @@ module ParallelTests
|
|
34
35
|
private
|
35
36
|
|
36
37
|
def with_locked_log
|
37
|
-
File.open(logfile, File::RDWR|File::CREAT) do |logfile|
|
38
|
+
File.open(logfile, File::RDWR | File::CREAT) do |logfile|
|
38
39
|
logfile.flock(File::LOCK_EX)
|
39
40
|
yield logfile
|
40
41
|
end
|
@@ -59,7 +60,7 @@ module ParallelTests
|
|
59
60
|
end
|
60
61
|
|
61
62
|
def message(test, delta)
|
62
|
-
return unless method = test.public_instance_methods(true).detect { |
|
63
|
+
return unless method = test.public_instance_methods(true).detect { |m| m =~ /^test_/ }
|
63
64
|
filename = test.instance_method(method).source_location.first.sub("#{Dir.pwd}/", "")
|
64
65
|
"#{filename}:#{delta}"
|
65
66
|
end
|
@@ -74,22 +75,26 @@ end
|
|
74
75
|
|
75
76
|
if defined?(Minitest::Runnable) # Minitest 5
|
76
77
|
class << Minitest::Runnable
|
77
|
-
prepend(
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
prepend(
|
79
|
+
Module.new do
|
80
|
+
def run(*)
|
81
|
+
ParallelTests::Test::RuntimeLogger.log_test_run(self) do
|
82
|
+
super
|
83
|
+
end
|
81
84
|
end
|
82
85
|
end
|
83
|
-
|
86
|
+
)
|
84
87
|
end
|
85
88
|
|
86
89
|
class << Minitest
|
87
|
-
prepend(
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
90
|
+
prepend(
|
91
|
+
Module.new do
|
92
|
+
def run(*args)
|
93
|
+
result = super
|
94
|
+
ParallelTests::Test::RuntimeLogger.unique_log
|
95
|
+
result
|
96
|
+
end
|
92
97
|
end
|
93
|
-
|
98
|
+
)
|
94
99
|
end
|
95
100
|
end
|
data/lib/parallel_tests.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "parallel"
|
2
3
|
require "parallel_tests/railtie" if defined? Rails::Railtie
|
3
4
|
require "rbconfig"
|
@@ -17,21 +18,19 @@ module ParallelTests
|
|
17
18
|
count,
|
18
19
|
ENV["PARALLEL_TEST_PROCESSORS"],
|
19
20
|
Parallel.processor_count
|
20
|
-
].detect{|c|
|
21
|
+
].detect { |c| !c.to_s.strip.empty? }.to_i
|
21
22
|
end
|
22
23
|
|
23
24
|
def with_pid_file
|
24
25
|
Tempfile.open('parallel_tests-pidfile') do |f|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@pids = nil
|
34
|
-
end
|
26
|
+
ENV['PARALLEL_PID_FILE'] = f.path
|
27
|
+
# Pids object should be created before threads will start adding pids to it
|
28
|
+
# Otherwise we would have to use Mutex to prevent creation of several instances
|
29
|
+
@pids = pids
|
30
|
+
yield
|
31
|
+
ensure
|
32
|
+
ENV['PARALLEL_PID_FILE'] = nil
|
33
|
+
@pids = nil
|
35
34
|
end
|
36
35
|
end
|
37
36
|
|
@@ -57,7 +56,8 @@ module ParallelTests
|
|
57
56
|
until !File.directory?(current) || current == previous
|
58
57
|
filename = File.join(current, "Gemfile")
|
59
58
|
return true if File.exist?(filename)
|
60
|
-
|
59
|
+
previous = current
|
60
|
+
current = File.expand_path("..", current)
|
61
61
|
end
|
62
62
|
|
63
63
|
false
|
@@ -76,7 +76,7 @@ module ParallelTests
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def with_ruby_binary(command)
|
79
|
-
WINDOWS ?
|
79
|
+
WINDOWS ? [RUBY_BINARY, '--', command] : [command]
|
80
80
|
end
|
81
81
|
|
82
82
|
def wait_for_other_processes_to_finish
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parallel_tests
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: parallel
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
-
description:
|
27
|
+
description:
|
28
28
|
email: michael@grosser.it
|
29
29
|
executables:
|
30
30
|
- parallel_spinach
|
@@ -68,10 +68,10 @@ licenses:
|
|
68
68
|
- MIT
|
69
69
|
metadata:
|
70
70
|
bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
|
71
|
-
documentation_uri: https://github.com/grosser/parallel_tests/blob/
|
72
|
-
source_code_uri: https://github.com/grosser/parallel_tests/tree/
|
71
|
+
documentation_uri: https://github.com/grosser/parallel_tests/blob/v4.2.0/Readme.md
|
72
|
+
source_code_uri: https://github.com/grosser/parallel_tests/tree/v4.2.0
|
73
73
|
wiki_uri: https://github.com/grosser/parallel_tests/wiki
|
74
|
-
post_install_message:
|
74
|
+
post_install_message:
|
75
75
|
rdoc_options: []
|
76
76
|
require_paths:
|
77
77
|
- lib
|
@@ -79,15 +79,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 2.
|
82
|
+
version: 2.7.0
|
83
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
84
|
requirements:
|
85
85
|
- - ">="
|
86
86
|
- !ruby/object:Gem::Version
|
87
87
|
version: '0'
|
88
88
|
requirements: []
|
89
|
-
rubygems_version: 3.
|
90
|
-
signing_key:
|
89
|
+
rubygems_version: 3.3.3
|
90
|
+
signing_key:
|
91
91
|
specification_version: 4
|
92
92
|
summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel
|
93
93
|
test_files: []
|