parallel_tests 3.3.0 → 4.2.0
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/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: []
|