parallel_tests 3.4.0 → 4.3.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 +58 -32
- 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 +161 -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 +54 -7
- data/lib/parallel_tests/pids.rb +5 -4
- data/lib/parallel_tests/railtie.rb +1 -0
- data/lib/parallel_tests/rspec/failures_logger.rb +6 -14
- data/lib/parallel_tests/rspec/logger_base.rb +9 -9
- data/lib/parallel_tests/rspec/runner.rb +21 -22
- data/lib/parallel_tests/rspec/runtime_logger.rb +14 -13
- 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
data/lib/parallel_tests/tasks.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'rake'
|
2
3
|
require 'shellwords'
|
3
4
|
|
@@ -8,16 +9,8 @@ module ParallelTests
|
|
8
9
|
'test'
|
9
10
|
end
|
10
11
|
|
11
|
-
def rake_bin
|
12
|
-
# Prevent 'Exec format error' Errno::ENOEXEC on Windows
|
13
|
-
return "rake" if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
14
|
-
binstub_path = File.join('bin', 'rake')
|
15
|
-
return binstub_path if File.exist?(binstub_path)
|
16
|
-
"rake"
|
17
|
-
end
|
18
|
-
|
19
12
|
def load_lib
|
20
|
-
$LOAD_PATH << File.expand_path(
|
13
|
+
$LOAD_PATH << File.expand_path('..', __dir__)
|
21
14
|
require "parallel_tests"
|
22
15
|
end
|
23
16
|
|
@@ -27,13 +20,17 @@ module ParallelTests
|
|
27
20
|
end
|
28
21
|
end
|
29
22
|
|
30
|
-
def run_in_parallel(cmd, options={})
|
23
|
+
def run_in_parallel(cmd, options = {})
|
31
24
|
load_lib
|
32
|
-
|
25
|
+
|
33
26
|
# Using the relative path to find the binary allow to run a specific version of it
|
34
|
-
executable = File.expand_path(
|
35
|
-
command =
|
36
|
-
|
27
|
+
executable = File.expand_path('../../bin/parallel_test', __dir__)
|
28
|
+
command = ParallelTests.with_ruby_binary(executable)
|
29
|
+
command += ['--exec', Shellwords.join(cmd)]
|
30
|
+
command += ['-n', options[:count]] unless options[:count].to_s.empty?
|
31
|
+
command << '--non-parallel' if options[:non_parallel]
|
32
|
+
|
33
|
+
abort unless system(*command)
|
37
34
|
end
|
38
35
|
|
39
36
|
# this is a crazy-complex solution for a very simple problem:
|
@@ -46,16 +43,14 @@ module ParallelTests
|
|
46
43
|
# - pipefail makes pipe fail with exitstatus of first failed command
|
47
44
|
# - pipefail is not supported in (zsh)
|
48
45
|
# - defining a new rake task like silence_schema would force users to load parallel_tests in test env
|
49
|
-
# - do not use ' since run_in_parallel uses them to quote stuff
|
50
46
|
# - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
|
51
47
|
def suppress_output(command, ignore_regex)
|
52
48
|
activate_pipefail = "set -o pipefail"
|
53
|
-
remove_ignored_lines = %
|
49
|
+
remove_ignored_lines = %{(grep -v #{Shellwords.escape(ignore_regex)} || true)}
|
54
50
|
|
55
|
-
if
|
56
|
-
|
57
|
-
|
58
|
-
%Q{/bin/bash -c '"'"'#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}'"'"'}
|
51
|
+
if system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null")
|
52
|
+
shell_command = "#{activate_pipefail} && (#{Shellwords.shelljoin(command)}) | #{remove_ignored_lines}"
|
53
|
+
['/bin/bash', '-c', shell_command]
|
59
54
|
else
|
60
55
|
command
|
61
56
|
end
|
@@ -83,12 +78,61 @@ module ParallelTests
|
|
83
78
|
# parallel:spec[2,models,options]
|
84
79
|
# parallel:spec[,models,options]
|
85
80
|
count = args.shift if args.first.to_s =~ /^\d*$/
|
86
|
-
num_processes = count.
|
81
|
+
num_processes = (count.to_s.empty? ? nil : Integer(count))
|
87
82
|
pattern = args.shift
|
88
83
|
options = args.shift
|
89
84
|
pass_through = args.shift
|
90
85
|
|
91
|
-
[num_processes, pattern
|
86
|
+
[num_processes, pattern, options, pass_through]
|
87
|
+
end
|
88
|
+
|
89
|
+
def schema_format_based_on_rails_version
|
90
|
+
if rails_7_or_greater?
|
91
|
+
ActiveRecord.schema_format
|
92
|
+
else
|
93
|
+
ActiveRecord::Base.schema_format
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def schema_type_based_on_rails_version
|
98
|
+
if rails_61_or_greater? || schema_format_based_on_rails_version == :ruby
|
99
|
+
"schema"
|
100
|
+
else
|
101
|
+
"structure"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_run_command(type, args)
|
106
|
+
count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
|
107
|
+
test_framework = {
|
108
|
+
'spec' => 'rspec',
|
109
|
+
'test' => 'test',
|
110
|
+
'features' => 'cucumber',
|
111
|
+
'features-spinach' => 'spinach'
|
112
|
+
}.fetch(type)
|
113
|
+
|
114
|
+
type = 'features' if test_framework == 'spinach'
|
115
|
+
|
116
|
+
# Using the relative path to find the binary allow to run a specific version of it
|
117
|
+
executable = File.expand_path('../../bin/parallel_test', __dir__)
|
118
|
+
executable = ParallelTests.with_ruby_binary(executable)
|
119
|
+
|
120
|
+
command = [*executable, type, '--type', test_framework]
|
121
|
+
command += ['-n', count.to_s] if count
|
122
|
+
command += ['--pattern', pattern] if pattern
|
123
|
+
command += ['--test-options', options] if options
|
124
|
+
command += Shellwords.shellsplit pass_through if pass_through
|
125
|
+
command
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def rails_7_or_greater?
|
131
|
+
Gem::Version.new(Rails.version) >= Gem::Version.new('7.0')
|
132
|
+
end
|
133
|
+
|
134
|
+
def rails_61_or_greater?
|
135
|
+
Gem::Version.new(Rails.version) >= Gem::Version.new('6.1.0')
|
92
136
|
end
|
93
137
|
end
|
94
138
|
end
|
@@ -96,30 +140,40 @@ end
|
|
96
140
|
|
97
141
|
namespace :parallel do
|
98
142
|
desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
|
99
|
-
task :setup, :count do |_,args|
|
100
|
-
command = "
|
143
|
+
task :setup, :count do |_, args|
|
144
|
+
command = [$0, "db:setup", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"]
|
101
145
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
102
146
|
end
|
103
147
|
|
104
148
|
desc "Create test databases via db:create --> parallel:create[num_cpus]"
|
105
|
-
task :create, :count do |_,args|
|
149
|
+
task :create, :count do |_, args|
|
106
150
|
ParallelTests::Tasks.run_in_parallel(
|
107
|
-
"
|
151
|
+
[$0, "db:create", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
152
|
+
args
|
153
|
+
)
|
108
154
|
end
|
109
155
|
|
110
156
|
desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
|
111
|
-
task :drop, :count do |_,args|
|
157
|
+
task :drop, :count do |_, args|
|
112
158
|
ParallelTests::Tasks.run_in_parallel(
|
113
|
-
|
114
|
-
|
159
|
+
[
|
160
|
+
$0,
|
161
|
+
"db:drop",
|
162
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
163
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
164
|
+
],
|
165
|
+
args
|
166
|
+
)
|
115
167
|
end
|
116
168
|
|
117
169
|
desc "Update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
118
|
-
task(:prepare, [:count]) do |_,args|
|
170
|
+
task(:prepare, [:count]) do |_, args|
|
119
171
|
ParallelTests::Tasks.check_for_pending_migrations
|
120
|
-
|
172
|
+
|
173
|
+
if defined?(ActiveRecord) && [:ruby, :sql].include?(ParallelTests::Tasks.schema_format_based_on_rails_version)
|
121
174
|
# fast: dump once, load in parallel
|
122
|
-
type =
|
175
|
+
type = ParallelTests::Tasks.schema_type_based_on_rails_version
|
176
|
+
|
123
177
|
Rake::Task["db:#{type}:dump"].invoke
|
124
178
|
|
125
179
|
# remove database connection to prevent "database is being accessed by other users"
|
@@ -128,82 +182,87 @@ namespace :parallel do
|
|
128
182
|
Rake::Task["parallel:load_#{type}"].invoke(args[:count])
|
129
183
|
else
|
130
184
|
# slow: dump and load in in serial
|
131
|
-
args = args.to_hash.merge(:
|
185
|
+
args = args.to_hash.merge(non_parallel: true) # normal merge returns nil
|
132
186
|
task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
|
133
|
-
ParallelTests::Tasks.run_in_parallel(
|
187
|
+
ParallelTests::Tasks.run_in_parallel([$0, task_name], args)
|
134
188
|
next
|
135
189
|
end
|
136
190
|
end
|
137
191
|
|
138
192
|
# when dumping/resetting takes too long
|
139
193
|
desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
140
|
-
task :migrate, :count do |_,args|
|
194
|
+
task :migrate, :count do |_, args|
|
141
195
|
ParallelTests::Tasks.run_in_parallel(
|
142
|
-
"
|
196
|
+
[$0, "db:migrate", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
197
|
+
args
|
198
|
+
)
|
143
199
|
end
|
144
200
|
|
145
201
|
desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
|
146
|
-
task :rollback, :count do |_,args|
|
202
|
+
task :rollback, :count do |_, args|
|
147
203
|
ParallelTests::Tasks.run_in_parallel(
|
148
|
-
"
|
204
|
+
[$0, "db:rollback", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
205
|
+
args
|
206
|
+
)
|
149
207
|
end
|
150
208
|
|
151
209
|
# just load the schema (good for integration server <-> no development db)
|
152
210
|
desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
153
|
-
task :load_schema, :count do |_,args|
|
154
|
-
command =
|
155
|
-
|
211
|
+
task :load_schema, :count do |_, args|
|
212
|
+
command = [
|
213
|
+
$0,
|
214
|
+
ParallelTests::Tasks.purge_before_load,
|
215
|
+
"db:schema:load",
|
216
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
217
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
218
|
+
]
|
156
219
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
157
220
|
end
|
158
221
|
|
159
222
|
# load the structure from the structure.sql file
|
160
|
-
|
161
|
-
|
223
|
+
# (faster for rails < 6.1, deprecated after and only configured by `ActiveRecord::Base.schema_format`)
|
224
|
+
desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]"
|
225
|
+
task :load_structure, :count do |_, args|
|
162
226
|
ParallelTests::Tasks.run_in_parallel(
|
163
|
-
|
164
|
-
|
227
|
+
[
|
228
|
+
$0,
|
229
|
+
ParallelTests::Tasks.purge_before_load,
|
230
|
+
"db:structure:load",
|
231
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
232
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
233
|
+
],
|
234
|
+
args
|
235
|
+
)
|
165
236
|
end
|
166
237
|
|
167
238
|
desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
168
|
-
task :seed, :count do |_,args|
|
239
|
+
task :seed, :count do |_, args|
|
169
240
|
ParallelTests::Tasks.run_in_parallel(
|
170
|
-
|
241
|
+
[
|
242
|
+
$0,
|
243
|
+
"db:seed",
|
244
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
245
|
+
],
|
246
|
+
args
|
247
|
+
)
|
171
248
|
end
|
172
249
|
|
173
250
|
desc "Launch given rake command in parallel"
|
174
251
|
task :rake, :command, :count do |_, args|
|
175
252
|
ParallelTests::Tasks.run_in_parallel(
|
176
|
-
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}
|
177
|
-
|
253
|
+
[$0, args.command, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
254
|
+
args
|
255
|
+
)
|
178
256
|
end
|
179
257
|
|
180
258
|
['test', 'spec', 'features', 'features-spinach'].each do |type|
|
181
259
|
desc "Run #{type} in parallel with parallel:#{type}[num_cpus]"
|
182
|
-
task type, [:count, :pattern, :options, :pass_through] do |
|
260
|
+
task type, [:count, :pattern, :options, :pass_through] do |_t, args|
|
183
261
|
ParallelTests::Tasks.check_for_pending_migrations
|
184
262
|
ParallelTests::Tasks.load_lib
|
263
|
+
command = ParallelTests::Tasks.build_run_command(type, args)
|
185
264
|
|
186
|
-
|
187
|
-
test_framework = {
|
188
|
-
'spec' => 'rspec',
|
189
|
-
'test' => 'test',
|
190
|
-
'features' => 'cucumber',
|
191
|
-
'features-spinach' => 'spinach',
|
192
|
-
}[type]
|
193
|
-
|
194
|
-
if test_framework == 'spinach'
|
195
|
-
type = 'features'
|
196
|
-
end
|
197
|
-
# Using the relative path to find the binary allow to run a specific version of it
|
198
|
-
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
199
|
-
|
200
|
-
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
|
201
|
-
"--type #{test_framework} " \
|
202
|
-
"-n #{count} " \
|
203
|
-
"--pattern '#{pattern}' " \
|
204
|
-
"--test-options '#{options}' " \
|
205
|
-
"#{pass_through}"
|
206
|
-
abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
265
|
+
abort unless system(*command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
207
266
|
end
|
208
267
|
end
|
209
268
|
end
|
@@ -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 = 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
|