parallel_tests 3.3.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/test/runner"
2
3
 
3
4
  module ParallelTests
@@ -6,24 +7,26 @@ module ParallelTests
6
7
  DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
7
8
  class << self
8
9
  def run_tests(test_files, process_number, num_processes, options)
9
- exe = executable # expensive, so we cache
10
- cmd = [exe, options[:test_options], color, spec_opts, *test_files].compact.join(" ")
10
+ cmd = [*executable, *options[:test_options], *color, *spec_opts, *test_files]
11
11
  execute_command(cmd, process_number, num_processes, options)
12
12
  end
13
13
 
14
14
  def determine_executable
15
- case
16
- when File.exist?("bin/rspec")
15
+ if File.exist?("bin/rspec")
17
16
  ParallelTests.with_ruby_binary("bin/rspec")
18
- when ParallelTests.bundler_enabled?
19
- "bundle exec rspec"
17
+ elsif ParallelTests.bundler_enabled?
18
+ ["bundle", "exec", "rspec"]
20
19
  else
21
- "rspec"
20
+ ["rspec"]
22
21
  end
23
22
  end
24
23
 
25
24
  def runtime_log
26
- 'tmp/parallel_runtime_rspec.log'
25
+ "tmp/parallel_runtime_rspec.log"
26
+ end
27
+
28
+ def default_test_folder
29
+ "spec"
27
30
  end
28
31
 
29
32
  def test_file_name
@@ -44,26 +47,36 @@ module ParallelTests
44
47
  # --order rand:1234
45
48
  # --order random:1234
46
49
  def command_with_seed(cmd, seed)
47
- clean = cmd.sub(/\s--(seed\s+\d+|order\s+rand(om)?(:\d+)?)\b/, '')
48
- "#{clean} --seed #{seed}"
50
+ clean = remove_command_arguments(cmd, '--seed', '--order')
51
+ [*clean, '--seed', seed]
49
52
  end
50
53
 
54
+ # Summarize results from threads and colorize results based on failure and pending counts.
55
+ #
56
+ def summarize_results(results)
57
+ text = super
58
+ return text unless $stdout.tty?
59
+ sums = sum_up_results(results)
60
+ color =
61
+ if sums['failure'] > 0
62
+ 31 # red
63
+ elsif sums['pending'] > 0
64
+ 33 # yellow
65
+ else
66
+ 32 # green
67
+ end
68
+ "\e[#{color}m#{text}\e[0m"
69
+ end
51
70
 
52
71
  private
53
72
 
54
- # so it can be stubbed....
55
- def run(cmd)
56
- `#{cmd}`
57
- end
58
-
59
73
  def color
60
- '--color --tty' if $stdout.tty?
74
+ ['--color', '--tty'] if $stdout.tty?
61
75
  end
62
76
 
63
77
  def spec_opts
64
- options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
65
- return unless options_file
66
- "-O #{options_file}"
78
+ options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect { |f| File.file?(f) }
79
+ ["-O", options_file] if options_file
67
80
  end
68
81
  end
69
82
  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(*args);end
31
- def dump_failures(*args);end
32
- def dump_failure(*args);end
33
- def dump_pending(*args);end
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(*args)
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(/^#{Regexp.escape Dir.pwd}\//,'').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
@@ -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(File.join(File.dirname(__FILE__), '..'))
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
- count = " -n #{options[:count]}" unless options[:count].to_s.empty?
25
+
33
26
  # Using the relative path to find the binary allow to run a specific version of it
34
- executable = File.expand_path("../../../bin/parallel_test", __FILE__)
35
- command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} --exec '#{cmd}'#{count}#{' --non-parallel' if options[:non_parallel]}"
36
- abort unless system(command)
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 = %Q{(grep -v "#{ignore_regex}" || test 1)}
49
+ remove_ignored_lines = %{(grep -v #{Shellwords.escape(ignore_regex)} || true)}
54
50
 
55
- if File.executable?('/bin/bash') && system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null && test 1")
56
- # We need to shell escape single quotes (' becomes '"'"') because
57
- # run_in_parallel wraps command in single quotes
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.to_i unless count.to_s.empty?
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.to_s, options.to_s, pass_through.to_s]
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 = "#{ParallelTests::Tasks.rake_bin} db:setup RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
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
- "#{ParallelTests::Tasks.rake_bin} db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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
- "#{ParallelTests::Tasks.rake_bin} db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env} " \
114
- "DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args)
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
- if defined?(ActiveRecord::Base) && [:ruby, :sql].include?(ActiveRecord::Base.schema_format)
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 = (ActiveRecord::Base.schema_format == :ruby ? "schema" : "structure")
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(:non_parallel => true) # normal merge returns nil
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("#{ParallelTests::Tasks.rake_bin} #{task_name}", args)
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
- "#{ParallelTests::Tasks.rake_bin} db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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
- "#{ParallelTests::Tasks.rake_bin} db:rollback RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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 = "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
155
- "db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
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
- desc "Load structure for test databases via db:structure:load --> parallel:load_structure[num_cpus]"
161
- task :load_structure, :count do |_,args|
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
- "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
164
- "db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args)
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
- "#{ParallelTests::Tasks.rake_bin} db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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} #{ParallelTests::Tasks.rake_bin} " \
177
- "#{args.command}", args)
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 |t, args|
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
- count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
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