parallel_tests 2.28.0 → 3.7.3
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 +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
|