parallel_tests 2.28.0 → 3.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +63 -36
- 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 +133 -68
- data/lib/parallel_tests/cucumber/failures_logger.rb +1 -1
- data/lib/parallel_tests/cucumber/features_with_steps.rb +32 -0
- data/lib/parallel_tests/cucumber/runner.rb +8 -5
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +18 -16
- data/lib/parallel_tests/cucumber/scenarios.rb +20 -30
- data/lib/parallel_tests/gherkin/io.rb +2 -3
- data/lib/parallel_tests/gherkin/listener.rb +10 -12
- data/lib/parallel_tests/gherkin/runner.rb +22 -23
- data/lib/parallel_tests/gherkin/runtime_logger.rb +3 -2
- data/lib/parallel_tests/grouper.rb +92 -28
- data/lib/parallel_tests/pids.rb +4 -3
- 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 +27 -12
- 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 +81 -40
- data/lib/parallel_tests/test/runner.rb +55 -38
- data/lib/parallel_tests/test/runtime_logger.rb +19 -14
- data/lib/parallel_tests/version.rb +2 -1
- data/lib/parallel_tests.rb +12 -12
- metadata +11 -7
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module ParallelTests
|
2
3
|
module RSpec
|
3
4
|
end
|
@@ -13,26 +14,27 @@ class ParallelTests::RSpec::LoggerBase < RSpec::Core::Formatters::BaseTextFormat
|
|
13
14
|
|
14
15
|
@output ||= args[0]
|
15
16
|
|
16
|
-
|
17
|
+
case @output
|
18
|
+
when String # a path ?
|
17
19
|
FileUtils.mkdir_p(File.dirname(@output))
|
18
|
-
File.open(@output, 'w'){} # overwrite previous results
|
20
|
+
File.open(@output, 'w') {} # overwrite previous results
|
19
21
|
@output = File.open(@output, 'a')
|
20
|
-
|
22
|
+
when File # close and restart in append mode
|
21
23
|
@output.close
|
22
24
|
@output = File.open(@output.path, 'a')
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
|
-
#stolen from Rspec
|
27
|
-
def close(*
|
28
|
-
@output.close
|
28
|
+
# stolen from Rspec
|
29
|
+
def close(*)
|
30
|
+
@output.close if (IO === @output) & (@output != $stdout)
|
29
31
|
end
|
30
32
|
|
31
33
|
protected
|
32
34
|
|
33
35
|
# do not let multiple processes get in each others way
|
34
36
|
def lock_output
|
35
|
-
if
|
37
|
+
if @output.is_a?(File)
|
36
38
|
begin
|
37
39
|
@output.flock File::LOCK_EX
|
38
40
|
yield
|
@@ -1,11 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "parallel_tests/test/runner"
|
2
3
|
|
3
4
|
module ParallelTests
|
4
5
|
module RSpec
|
5
6
|
class Runner < ParallelTests::Test::Runner
|
6
7
|
DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
|
7
|
-
NAME = 'RSpec'
|
8
|
-
|
9
8
|
class << self
|
10
9
|
def run_tests(test_files, process_number, num_processes, options)
|
11
10
|
exe = executable # expensive, so we cache
|
@@ -14,21 +13,21 @@ module ParallelTests
|
|
14
13
|
end
|
15
14
|
|
16
15
|
def determine_executable
|
17
|
-
|
18
|
-
when File.exist?("bin/rspec")
|
16
|
+
if File.exist?("bin/rspec")
|
19
17
|
ParallelTests.with_ruby_binary("bin/rspec")
|
20
|
-
|
21
|
-
|
22
|
-
"bundle exec #{cmd}"
|
18
|
+
elsif ParallelTests.bundler_enabled?
|
19
|
+
"bundle exec rspec"
|
23
20
|
else
|
24
|
-
|
21
|
+
"rspec"
|
25
22
|
end
|
26
|
-
|
27
|
-
cmd or raise("Can't find executables rspec or spec")
|
28
23
|
end
|
29
24
|
|
30
25
|
def runtime_log
|
31
|
-
|
26
|
+
"tmp/parallel_runtime_rspec.log"
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_test_folder
|
30
|
+
"spec"
|
32
31
|
end
|
33
32
|
|
34
33
|
def test_file_name
|
@@ -53,6 +52,22 @@ module ParallelTests
|
|
53
52
|
"#{clean} --seed #{seed}"
|
54
53
|
end
|
55
54
|
|
55
|
+
# Summarize results from threads and colorize results based on failure and pending counts.
|
56
|
+
#
|
57
|
+
def summarize_results(results)
|
58
|
+
text = super
|
59
|
+
return text unless $stdout.tty?
|
60
|
+
sums = sum_up_results(results)
|
61
|
+
color =
|
62
|
+
if sums['failure'] > 0
|
63
|
+
31 # red
|
64
|
+
elsif sums['pending'] > 0
|
65
|
+
33 # yellow
|
66
|
+
else
|
67
|
+
32 # green
|
68
|
+
end
|
69
|
+
"\e[#{color}m#{text}\e[0m"
|
70
|
+
end
|
56
71
|
|
57
72
|
private
|
58
73
|
|
@@ -66,7 +81,7 @@ module ParallelTests
|
|
66
81
|
end
|
67
82
|
|
68
83
|
def spec_opts
|
69
|
-
options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
|
84
|
+
options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect { |f| File.file?(f) }
|
70
85
|
return unless options_file
|
71
86
|
"-O #{options_file}"
|
72
87
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'parallel_tests'
|
2
3
|
require 'parallel_tests/rspec/logger_base'
|
3
4
|
|
@@ -8,9 +9,7 @@ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
|
|
8
9
|
@group_nesting = 0
|
9
10
|
end
|
10
11
|
|
11
|
-
unless RSPEC_2
|
12
|
-
RSpec::Core::Formatters.register self, :example_group_started, :example_group_finished, :start_dump
|
13
|
-
end
|
12
|
+
RSpec::Core::Formatters.register self, :example_group_started, :example_group_finished, :start_dump unless RSPEC_2
|
14
13
|
|
15
14
|
def example_group_started(example_group)
|
16
15
|
@time = ParallelTests.now if @group_nesting == 0
|
@@ -27,16 +26,19 @@ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
|
|
27
26
|
super if defined?(super)
|
28
27
|
end
|
29
28
|
|
30
|
-
def dump_summary(*
|
31
|
-
|
32
|
-
def
|
33
|
-
|
29
|
+
def dump_summary(*); end
|
30
|
+
|
31
|
+
def dump_failures(*); end
|
32
|
+
|
33
|
+
def dump_failure(*); end
|
34
|
+
|
35
|
+
def dump_pending(*); end
|
34
36
|
|
35
|
-
def start_dump(*
|
36
|
-
return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
|
37
|
+
def start_dump(*)
|
38
|
+
return unless ENV['TEST_ENV_NUMBER'] # only record when running in parallel
|
37
39
|
lock_output do
|
38
40
|
@example_times.each do |file, time|
|
39
|
-
relative_path = file.sub(
|
41
|
+
relative_path = file.sub(%r{^#{Regexp.escape Dir.pwd}/}, '').sub(%r{^\./}, "")
|
40
42
|
@output.puts "#{relative_path}:#{time > 0 ? time : 0}"
|
41
43
|
end
|
42
44
|
end
|
@@ -1,9 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'parallel_tests/rspec/failures_logger'
|
2
3
|
|
3
4
|
class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase
|
4
|
-
unless RSPEC_2
|
5
|
-
RSpec::Core::Formatters.register self, :dump_failures
|
6
|
-
end
|
5
|
+
RSpec::Core::Formatters.register self, :dump_failures unless RSPEC_2
|
7
6
|
|
8
7
|
def dump_failures(*args)
|
9
8
|
lock_output { super }
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require "parallel_tests/gherkin/runner"
|
2
3
|
|
3
4
|
module ParallelTests
|
@@ -8,11 +9,14 @@ module ParallelTests
|
|
8
9
|
'spinach'
|
9
10
|
end
|
10
11
|
|
12
|
+
def default_test_folder
|
13
|
+
'features'
|
14
|
+
end
|
15
|
+
|
11
16
|
def runtime_logging
|
12
|
-
#Not Yet Supported
|
17
|
+
# Not Yet Supported
|
13
18
|
""
|
14
19
|
end
|
15
|
-
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
data/lib/parallel_tests/tasks.rb
CHANGED
@@ -1,10 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'rake'
|
3
|
+
require 'shellwords'
|
2
4
|
|
3
5
|
module ParallelTests
|
4
6
|
module Tasks
|
5
7
|
class << self
|
6
8
|
def rails_env
|
7
|
-
|
9
|
+
'test'
|
10
|
+
end
|
11
|
+
|
12
|
+
def rake_bin
|
13
|
+
# Prevent 'Exec format error' Errno::ENOEXEC on Windows
|
14
|
+
return "rake" if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
15
|
+
binstub_path = File.join('bin', 'rake')
|
16
|
+
return binstub_path if File.exist?(binstub_path)
|
17
|
+
"rake"
|
8
18
|
end
|
9
19
|
|
10
20
|
def load_lib
|
@@ -18,12 +28,13 @@ module ParallelTests
|
|
18
28
|
end
|
19
29
|
end
|
20
30
|
|
21
|
-
def run_in_parallel(cmd, options={})
|
31
|
+
def run_in_parallel(cmd, options = {})
|
22
32
|
load_lib
|
23
33
|
count = " -n #{options[:count]}" unless options[:count].to_s.empty?
|
24
34
|
# Using the relative path to find the binary allow to run a specific version of it
|
25
|
-
executable = File.expand_path(
|
26
|
-
|
35
|
+
executable = File.expand_path('../../bin/parallel_test', __dir__)
|
36
|
+
non_parallel = (options[:non_parallel] ? ' --non-parallel' : '')
|
37
|
+
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} --exec '#{cmd}'#{count}#{non_parallel}"
|
27
38
|
abort unless system(command)
|
28
39
|
end
|
29
40
|
|
@@ -41,12 +52,12 @@ module ParallelTests
|
|
41
52
|
# - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
|
42
53
|
def suppress_output(command, ignore_regex)
|
43
54
|
activate_pipefail = "set -o pipefail"
|
44
|
-
remove_ignored_lines = %
|
55
|
+
remove_ignored_lines = %{(grep -v "#{ignore_regex}" || test 1)}
|
45
56
|
|
46
57
|
if File.executable?('/bin/bash') && system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null && test 1")
|
47
58
|
# We need to shell escape single quotes (' becomes '"'"') because
|
48
59
|
# run_in_parallel wraps command in single quotes
|
49
|
-
%
|
60
|
+
%{/bin/bash -c '"'"'#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}'"'"'}
|
50
61
|
else
|
51
62
|
command
|
52
63
|
end
|
@@ -74,7 +85,7 @@ module ParallelTests
|
|
74
85
|
# parallel:spec[2,models,options]
|
75
86
|
# parallel:spec[,models,options]
|
76
87
|
count = args.shift if args.first.to_s =~ /^\d*$/
|
77
|
-
num_processes = count.
|
88
|
+
num_processes = (count.to_s.empty? ? nil : Integer(count))
|
78
89
|
pattern = args.shift
|
79
90
|
options = args.shift
|
80
91
|
pass_through = args.shift
|
@@ -87,73 +98,104 @@ end
|
|
87
98
|
|
88
99
|
namespace :parallel do
|
89
100
|
desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
|
90
|
-
task :setup, :count do |_,args|
|
91
|
-
command = "
|
101
|
+
task :setup, :count do |_, args|
|
102
|
+
command = "#{ParallelTests::Tasks.rake_bin} db:setup RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
92
103
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
93
104
|
end
|
94
105
|
|
95
106
|
desc "Create test databases via db:create --> parallel:create[num_cpus]"
|
96
|
-
task :create, :count do |_,args|
|
97
|
-
ParallelTests::Tasks.run_in_parallel(
|
107
|
+
task :create, :count do |_, args|
|
108
|
+
ParallelTests::Tasks.run_in_parallel(
|
109
|
+
"#{ParallelTests::Tasks.rake_bin} db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
|
110
|
+
)
|
98
111
|
end
|
99
112
|
|
100
113
|
desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
|
101
|
-
task :drop, :count do |_,args|
|
102
|
-
ParallelTests::Tasks.run_in_parallel(
|
114
|
+
task :drop, :count do |_, args|
|
115
|
+
ParallelTests::Tasks.run_in_parallel(
|
116
|
+
"#{ParallelTests::Tasks.rake_bin} db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env} " \
|
117
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args
|
118
|
+
)
|
103
119
|
end
|
104
120
|
|
105
121
|
desc "Update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
106
|
-
task(:prepare, [:count]) do |_,args|
|
122
|
+
task(:prepare, [:count]) do |_, args|
|
107
123
|
ParallelTests::Tasks.check_for_pending_migrations
|
108
|
-
if defined?(ActiveRecord) && ActiveRecord::Base.schema_format
|
109
|
-
# dump
|
110
|
-
|
111
|
-
|
124
|
+
if defined?(ActiveRecord::Base) && [:ruby, :sql].include?(ActiveRecord::Base.schema_format)
|
125
|
+
# fast: dump once, load in parallel
|
126
|
+
type =
|
127
|
+
if Gem::Version.new(Rails.version) >= Gem::Version.new('6.1.0')
|
128
|
+
"schema"
|
129
|
+
else
|
130
|
+
ActiveRecord::Base.schema_format == :ruby ? "schema" : "structure"
|
131
|
+
end
|
132
|
+
|
133
|
+
Rake::Task["db:#{type}:dump"].invoke
|
134
|
+
|
135
|
+
# remove database connection to prevent "database is being accessed by other users"
|
136
|
+
ActiveRecord::Base.remove_connection if ActiveRecord::Base.configurations.any?
|
137
|
+
|
138
|
+
Rake::Task["parallel:load_#{type}"].invoke(args[:count])
|
112
139
|
else
|
113
|
-
#
|
114
|
-
args = args.to_hash.merge(:
|
115
|
-
|
116
|
-
ParallelTests::Tasks.run_in_parallel("
|
140
|
+
# slow: dump and load in in serial
|
141
|
+
args = args.to_hash.merge(non_parallel: true) # normal merge returns nil
|
142
|
+
task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
|
143
|
+
ParallelTests::Tasks.run_in_parallel("#{ParallelTests::Tasks.rake_bin} #{task_name}", args)
|
144
|
+
next
|
117
145
|
end
|
118
146
|
end
|
119
147
|
|
120
148
|
# when dumping/resetting takes too long
|
121
149
|
desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
122
|
-
task :migrate, :count do |_,args|
|
123
|
-
ParallelTests::Tasks.run_in_parallel(
|
150
|
+
task :migrate, :count do |_, args|
|
151
|
+
ParallelTests::Tasks.run_in_parallel(
|
152
|
+
"#{ParallelTests::Tasks.rake_bin} db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
|
153
|
+
)
|
124
154
|
end
|
125
155
|
|
126
156
|
desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
|
127
|
-
task :rollback, :count do |_,args|
|
128
|
-
ParallelTests::Tasks.run_in_parallel(
|
157
|
+
task :rollback, :count do |_, args|
|
158
|
+
ParallelTests::Tasks.run_in_parallel(
|
159
|
+
"#{ParallelTests::Tasks.rake_bin} db:rollback RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
|
160
|
+
)
|
129
161
|
end
|
130
162
|
|
131
163
|
# just load the schema (good for integration server <-> no development db)
|
132
164
|
desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
133
|
-
task :load_schema, :count do |_,args|
|
134
|
-
command = "
|
165
|
+
task :load_schema, :count do |_, args|
|
166
|
+
command = "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
|
167
|
+
"db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
135
168
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
136
169
|
end
|
137
170
|
|
138
171
|
# load the structure from the structure.sql file
|
139
|
-
|
140
|
-
|
141
|
-
|
172
|
+
# (faster for rails < 6.1, deprecated after and only configured by `ActiveRecord::Base.schema_format`)
|
173
|
+
desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]"
|
174
|
+
task :load_structure, :count do |_, args|
|
175
|
+
ParallelTests::Tasks.run_in_parallel(
|
176
|
+
"#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
|
177
|
+
"db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args
|
178
|
+
)
|
142
179
|
end
|
143
180
|
|
144
181
|
desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
145
|
-
task :seed, :count do |_,args|
|
146
|
-
ParallelTests::Tasks.run_in_parallel(
|
182
|
+
task :seed, :count do |_, args|
|
183
|
+
ParallelTests::Tasks.run_in_parallel(
|
184
|
+
"#{ParallelTests::Tasks.rake_bin} db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
|
185
|
+
)
|
147
186
|
end
|
148
187
|
|
149
188
|
desc "Launch given rake command in parallel"
|
150
189
|
task :rake, :command, :count do |_, args|
|
151
|
-
ParallelTests::Tasks.run_in_parallel(
|
190
|
+
ParallelTests::Tasks.run_in_parallel(
|
191
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env} #{ParallelTests::Tasks.rake_bin} " \
|
192
|
+
"#{args.command}", args
|
193
|
+
)
|
152
194
|
end
|
153
195
|
|
154
196
|
['test', 'spec', 'features', 'features-spinach'].each do |type|
|
155
197
|
desc "Run #{type} in parallel with parallel:#{type}[num_cpus]"
|
156
|
-
task type, [:count, :pattern, :options, :pass_through] do |
|
198
|
+
task type, [:count, :pattern, :options, :pass_through] do |_t, args|
|
157
199
|
ParallelTests::Tasks.check_for_pending_migrations
|
158
200
|
ParallelTests::Tasks.load_lib
|
159
201
|
|
@@ -162,16 +204,15 @@ namespace :parallel do
|
|
162
204
|
'spec' => 'rspec',
|
163
205
|
'test' => 'test',
|
164
206
|
'features' => 'cucumber',
|
165
|
-
'features-spinach' => 'spinach'
|
207
|
+
'features-spinach' => 'spinach'
|
166
208
|
}[type]
|
167
209
|
|
168
|
-
if test_framework == 'spinach'
|
169
|
-
type = 'features'
|
170
|
-
end
|
210
|
+
type = 'features' if test_framework == 'spinach'
|
171
211
|
# Using the relative path to find the binary allow to run a specific version of it
|
172
212
|
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
173
213
|
|
174
|
-
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type}
|
214
|
+
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
|
215
|
+
"--type #{test_framework} " \
|
175
216
|
"-n #{count} " \
|
176
217
|
"--pattern '#{pattern}' " \
|
177
218
|
"--test-options '#{options}' " \
|
@@ -1,17 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'parallel_tests'
|
2
3
|
|
3
4
|
module ParallelTests
|
4
5
|
module Test
|
5
6
|
class Runner
|
6
|
-
NAME = 'Test'
|
7
|
-
|
8
7
|
class << self
|
9
8
|
# --- usually overwritten by other runners
|
10
9
|
|
11
|
-
def name
|
12
|
-
NAME
|
13
|
-
end
|
14
|
-
|
15
10
|
def runtime_log
|
16
11
|
'tmp/parallel_runtime_test.log'
|
17
12
|
end
|
@@ -20,12 +15,16 @@ module ParallelTests
|
|
20
15
|
/_(test|spec).rb$/
|
21
16
|
end
|
22
17
|
|
18
|
+
def default_test_folder
|
19
|
+
"test"
|
20
|
+
end
|
21
|
+
|
23
22
|
def test_file_name
|
24
23
|
"test"
|
25
24
|
end
|
26
25
|
|
27
26
|
def run_tests(test_files, process_number, num_processes, options)
|
28
|
-
require_list = test_files.map { |file| file.
|
27
|
+
require_list = test_files.map { |file| file.gsub(" ", "\\ ") }.join(" ")
|
29
28
|
cmd = "#{executable} -Itest -e '%w[#{require_list}].each { |f| require %{./\#{f}} }' -- #{options[:test_options]}"
|
30
29
|
execute_command(cmd, process_number, num_processes, options)
|
31
30
|
end
|
@@ -38,7 +37,7 @@ module ParallelTests
|
|
38
37
|
# --- usually used by other runners
|
39
38
|
|
40
39
|
# finds all tests and partitions them into groups
|
41
|
-
def tests_in_groups(tests, num_groups, options={})
|
40
|
+
def tests_in_groups(tests, num_groups, options = {})
|
42
41
|
tests = tests_with_size(tests, options)
|
43
42
|
Grouper.in_even_groups_by_size(tests, num_groups, options)
|
44
43
|
end
|
@@ -52,10 +51,17 @@ module ParallelTests
|
|
52
51
|
when :filesize
|
53
52
|
sort_by_filesize(tests)
|
54
53
|
when :runtime
|
55
|
-
sort_by_runtime(
|
54
|
+
sort_by_runtime(
|
55
|
+
tests, runtimes(tests, options),
|
56
|
+
options.merge(allowed_missing: (options[:allowed_missing_percent] || 50) / 100.0)
|
57
|
+
)
|
56
58
|
when nil
|
57
59
|
# use recorded test runtime if we got enough data
|
58
|
-
runtimes =
|
60
|
+
runtimes = begin
|
61
|
+
runtimes(tests, options)
|
62
|
+
rescue StandardError
|
63
|
+
[]
|
64
|
+
end
|
59
65
|
if runtimes.size * 1.5 > tests.size
|
60
66
|
puts "Using recorded test runtime"
|
61
67
|
sort_by_runtime(tests, runtimes)
|
@@ -73,12 +79,12 @@ module ParallelTests
|
|
73
79
|
env = (options[:env] || {}).merge(
|
74
80
|
"TEST_ENV_NUMBER" => test_env_number(process_number, options).to_s,
|
75
81
|
"PARALLEL_TEST_GROUPS" => num_processes.to_s,
|
76
|
-
"PARALLEL_PID_FILE" => ParallelTests.pid_file_path
|
82
|
+
"PARALLEL_PID_FILE" => ParallelTests.pid_file_path
|
77
83
|
)
|
78
84
|
cmd = "nice #{cmd}" if options[:nice]
|
79
85
|
cmd = "#{cmd} 2>&1" if options[:combine_stderr]
|
80
86
|
|
81
|
-
puts cmd if options
|
87
|
+
puts cmd if report_process_command?(options) && !options[:serialize_stdout]
|
82
88
|
|
83
89
|
execute_command_and_capture_output(env, cmd, options)
|
84
90
|
end
|
@@ -92,11 +98,11 @@ module ParallelTests
|
|
92
98
|
end
|
93
99
|
ParallelTests.pids.delete(pid) if pid
|
94
100
|
exitstatus = $?.exitstatus
|
95
|
-
seed = output[/seed (\d+)/,1]
|
101
|
+
seed = output[/seed (\d+)/, 1]
|
96
102
|
|
97
|
-
output = [cmd, output].join("\n") if options
|
103
|
+
output = [cmd, output].join("\n") if report_process_command?(options) && options[:serialize_stdout]
|
98
104
|
|
99
|
-
{:
|
105
|
+
{ stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
|
100
106
|
end
|
101
107
|
|
102
108
|
def find_results(test_output)
|
@@ -108,7 +114,7 @@ module ParallelTests
|
|
108
114
|
end.compact
|
109
115
|
end
|
110
116
|
|
111
|
-
def test_env_number(process_number, options={})
|
117
|
+
def test_env_number(process_number, options = {})
|
112
118
|
if process_number == 0 && !options[:first_is_1]
|
113
119
|
''
|
114
120
|
else
|
@@ -118,7 +124,7 @@ module ParallelTests
|
|
118
124
|
|
119
125
|
def summarize_results(results)
|
120
126
|
sums = sum_up_results(results)
|
121
|
-
sums.sort.map{|word, number|
|
127
|
+
sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
122
128
|
end
|
123
129
|
|
124
130
|
# remove old seed and add new seed
|
@@ -138,19 +144,18 @@ module ParallelTests
|
|
138
144
|
end
|
139
145
|
|
140
146
|
def sum_up_results(results)
|
141
|
-
results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
|
147
|
+
results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
|
142
148
|
counts = results.scan(/(\d+) (\w+)/)
|
143
|
-
counts.
|
149
|
+
counts.each_with_object(Hash.new(0)) do |(number, word), sum|
|
144
150
|
sum[word] += number.to_i
|
145
|
-
sum
|
146
151
|
end
|
147
152
|
end
|
148
153
|
|
149
154
|
# read output of the process and print it in chunks
|
150
|
-
def capture_output(out, env, options={})
|
151
|
-
result = ""
|
152
|
-
|
153
|
-
|
155
|
+
def capture_output(out, env, options = {})
|
156
|
+
result = +""
|
157
|
+
begin
|
158
|
+
loop do
|
154
159
|
read = out.readpartial(1000000) # read whatever chunk we can get
|
155
160
|
if Encoding.default_internal
|
156
161
|
read = read.force_encoding(Encoding.default_internal)
|
@@ -163,11 +168,13 @@ module ParallelTests
|
|
163
168
|
$stdout.flush
|
164
169
|
end
|
165
170
|
end
|
166
|
-
|
171
|
+
rescue EOFError
|
172
|
+
nil
|
173
|
+
end
|
167
174
|
result
|
168
175
|
end
|
169
176
|
|
170
|
-
def sort_by_runtime(tests, runtimes, options={})
|
177
|
+
def sort_by_runtime(tests, runtimes, options = {})
|
171
178
|
allowed_missing = options[:allowed_missing] || 1.0
|
172
179
|
allowed_missing = tests.size * allowed_missing
|
173
180
|
|
@@ -177,20 +184,14 @@ module ParallelTests
|
|
177
184
|
allowed_missing -= 1 unless time = runtimes[test]
|
178
185
|
if allowed_missing < 0
|
179
186
|
log = options[:runtime_log] || runtime_log
|
180
|
-
raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update it."
|
187
|
+
raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
|
181
188
|
end
|
182
189
|
[test, time]
|
183
190
|
end
|
184
191
|
|
185
|
-
if options[:verbose]
|
186
|
-
puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
|
187
|
-
end
|
192
|
+
puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests" if options[:verbose]
|
188
193
|
|
189
|
-
|
190
|
-
known, unknown = tests.partition(&:last)
|
191
|
-
average = (known.any? ? known.map!(&:last).inject(:+) / known.size : 1)
|
192
|
-
unknown_runtime = options[:unknown_runtime] || average
|
193
|
-
unknown.each { |set| set[1] = unknown_runtime }
|
194
|
+
set_unknown_runtime tests, options
|
194
195
|
end
|
195
196
|
|
196
197
|
def runtimes(tests, options)
|
@@ -198,7 +199,7 @@ module ParallelTests
|
|
198
199
|
lines = File.read(log).split("\n")
|
199
200
|
lines.each_with_object({}) do |line, times|
|
200
201
|
test, _, time = line.rpartition(':')
|
201
|
-
next unless test
|
202
|
+
next unless test && time
|
202
203
|
times[test] = time.to_f if tests.include?(test)
|
203
204
|
end
|
204
205
|
end
|
@@ -225,7 +226,7 @@ module ParallelTests
|
|
225
226
|
end.uniq
|
226
227
|
end
|
227
228
|
|
228
|
-
def files_in_folder(folder, options={})
|
229
|
+
def files_in_folder(folder, options = {})
|
229
230
|
pattern = if options[:symlinks] == false # not nil or true
|
230
231
|
"**/*"
|
231
232
|
else
|
@@ -233,7 +234,23 @@ module ParallelTests
|
|
233
234
|
# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
|
234
235
|
"**{,/*/**}/*"
|
235
236
|
end
|
236
|
-
Dir[File.join(folder, pattern)].uniq
|
237
|
+
Dir[File.join(folder, pattern)].uniq.sort
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
# fill gaps with unknown-runtime if given, average otherwise
|
243
|
+
# NOTE: an optimization could be doing runtime by average runtime per file size, but would need file checks
|
244
|
+
def set_unknown_runtime(tests, options)
|
245
|
+
known, unknown = tests.partition(&:last)
|
246
|
+
return if unknown.empty?
|
247
|
+
unknown_runtime = options[:unknown_runtime] ||
|
248
|
+
(known.empty? ? 1 : known.map!(&:last).sum / known.size) # average
|
249
|
+
unknown.each { |set| set[1] = unknown_runtime }
|
250
|
+
end
|
251
|
+
|
252
|
+
def report_process_command?(options)
|
253
|
+
options[:verbose] || options[:verbose_process_command]
|
237
254
|
end
|
238
255
|
end
|
239
256
|
end
|