parallel_tests 2.21.3 → 4.2.1

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.
@@ -1,17 +1,15 @@
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
6
- NAME = 'Test'
8
+ RuntimeLogTooSmallError = Class.new(StandardError)
7
9
 
8
10
  class << self
9
11
  # --- usually overwritten by other runners
10
12
 
11
- def name
12
- NAME
13
- end
14
-
15
13
  def runtime_log
16
14
  'tmp/parallel_runtime_test.log'
17
15
  end
@@ -20,13 +18,24 @@ module ParallelTests
20
18
  /_(test|spec).rb$/
21
19
  end
22
20
 
21
+ def default_test_folder
22
+ "test"
23
+ end
24
+
23
25
  def test_file_name
24
26
  "test"
25
27
  end
26
28
 
27
29
  def run_tests(test_files, process_number, num_processes, options)
28
- require_list = test_files.map { |file| file.sub(" ", "\\ ") }.join(" ")
29
- cmd = "#{executable} -Itest -e '%w[#{require_list}].each { |f| require %{./\#{f}} }' -- #{options[:test_options]}"
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
+ ]
30
39
  execute_command(cmd, process_number, num_processes, options)
31
40
  end
32
41
 
@@ -38,7 +47,7 @@ module ParallelTests
38
47
  # --- usually used by other runners
39
48
 
40
49
  # finds all tests and partitions them into groups
41
- def tests_in_groups(tests, num_groups, options={})
50
+ def tests_in_groups(tests, num_groups, options = {})
42
51
  tests = tests_with_size(tests, options)
43
52
  Grouper.in_even_groups_by_size(tests, num_groups, options)
44
53
  end
@@ -52,12 +61,19 @@ module ParallelTests
52
61
  when :filesize
53
62
  sort_by_filesize(tests)
54
63
  when :runtime
55
- sort_by_runtime(tests, runtimes(tests, options), options.merge(allowed_missing: (options[:allowed_missing_percent] || 50) / 100.0))
64
+ sort_by_runtime(
65
+ tests, runtimes(tests, options),
66
+ options.merge(allowed_missing: (options[:allowed_missing_percent] || 50) / 100.0)
67
+ )
56
68
  when nil
57
69
  # use recorded test runtime if we got enough data
58
- runtimes = runtimes(tests, options) rescue []
70
+ runtimes = begin
71
+ runtimes(tests, options)
72
+ rescue StandardError
73
+ []
74
+ end
59
75
  if runtimes.size * 1.5 > tests.size
60
- puts "Using recorded test runtime"
76
+ puts "Using recorded test runtime" unless options[:quiet]
61
77
  sort_by_runtime(tests, runtimes)
62
78
  else
63
79
  sort_by_filesize(tests)
@@ -70,31 +86,44 @@ module ParallelTests
70
86
  end
71
87
 
72
88
  def execute_command(cmd, process_number, num_processes, options)
89
+ number = test_env_number(process_number, options).to_s
73
90
  env = (options[:env] || {}).merge(
74
- "TEST_ENV_NUMBER" => test_env_number(process_number, options).to_s,
91
+ "TEST_ENV_NUMBER" => number,
75
92
  "PARALLEL_TEST_GROUPS" => num_processes.to_s,
76
- "PARALLEL_PID_FILE" => ParallelTests.pid_file_path,
93
+ "PARALLEL_PID_FILE" => ParallelTests.pid_file_path
77
94
  )
78
- cmd = "nice #{cmd}" if options[:nice]
79
- cmd = "#{cmd} 2>&1" if options[:combine_stderr]
95
+ cmd = ["nice", *cmd] if options[:nice]
80
96
 
81
- puts cmd if options[:verbose]
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]
82
101
 
83
102
  execute_command_and_capture_output(env, cmd, options)
84
103
  end
85
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
+
86
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
+
87
114
  pid = nil
88
- output = IO.popen(env, cmd) do |io|
115
+ output = IO.popen(env, cmd, popen_options) do |io|
89
116
  pid = io.pid
90
117
  ParallelTests.pids.add(pid)
91
118
  capture_output(io, env, options)
92
119
  end
93
120
  ParallelTests.pids.delete(pid) if pid
94
121
  exitstatus = $?.exitstatus
95
- seed = output[/seed (\d+)/,1]
122
+ seed = output[/seed (\d+)/, 1]
96
123
 
97
- {:stdout => output, :exit_status => exitstatus, :command => cmd, :seed => seed}
124
+ output = "#{Shellwords.shelljoin(cmd)}\n#{output}" if report_process_command?(options) && options[:serialize_stdout]
125
+
126
+ { env: env, stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
98
127
  end
99
128
 
100
129
  def find_results(test_output)
@@ -106,7 +135,7 @@ module ParallelTests
106
135
  end.compact
107
136
  end
108
137
 
109
- def test_env_number(process_number, options={})
138
+ def test_env_number(process_number, options = {})
110
139
  if process_number == 0 && !options[:first_is_1]
111
140
  ''
112
141
  else
@@ -116,39 +145,42 @@ module ParallelTests
116
145
 
117
146
  def summarize_results(results)
118
147
  sums = sum_up_results(results)
119
- sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
148
+ sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
120
149
  end
121
150
 
122
151
  # remove old seed and add new seed
123
152
  def command_with_seed(cmd, seed)
124
- clean = cmd.sub(/\s--seed\s+\d+\b/, '')
125
- "#{clean} --seed #{seed}"
153
+ clean = remove_command_arguments(cmd, '--seed')
154
+ [*clean, '--seed', seed]
126
155
  end
127
156
 
128
157
  protected
129
158
 
130
159
  def executable
131
- ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable
160
+ if (executable = ENV['PARALLEL_TESTS_EXECUTABLE'])
161
+ [executable]
162
+ else
163
+ determine_executable
164
+ end
132
165
  end
133
166
 
134
167
  def determine_executable
135
- "ruby"
168
+ ["ruby"]
136
169
  end
137
170
 
138
171
  def sum_up_results(results)
139
- results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
172
+ results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
140
173
  counts = results.scan(/(\d+) (\w+)/)
141
- counts.inject(Hash.new(0)) do |sum, (number, word)|
174
+ counts.each_with_object(Hash.new(0)) do |(number, word), sum|
142
175
  sum[word] += number.to_i
143
- sum
144
176
  end
145
177
  end
146
178
 
147
179
  # read output of the process and print it in chunks
148
- def capture_output(out, env, options={})
149
- result = ""
150
- loop do
151
- begin
180
+ def capture_output(out, env, options = {})
181
+ result = +""
182
+ begin
183
+ loop do
152
184
  read = out.readpartial(1000000) # read whatever chunk we can get
153
185
  if Encoding.default_internal
154
186
  read = read.force_encoding(Encoding.default_internal)
@@ -161,11 +193,13 @@ module ParallelTests
161
193
  $stdout.flush
162
194
  end
163
195
  end
164
- end rescue EOFError
196
+ rescue EOFError
197
+ nil
198
+ end
165
199
  result
166
200
  end
167
201
 
168
- def sort_by_runtime(tests, runtimes, options={})
202
+ def sort_by_runtime(tests, runtimes, options = {})
169
203
  allowed_missing = options[:allowed_missing] || 1.0
170
204
  allowed_missing = tests.size * allowed_missing
171
205
 
@@ -175,20 +209,14 @@ module ParallelTests
175
209
  allowed_missing -= 1 unless time = runtimes[test]
176
210
  if allowed_missing < 0
177
211
  log = options[:runtime_log] || runtime_log
178
- raise "Runtime log file '#{log}' does not cointain sufficient data to sort #{tests.size} test files, please update it."
212
+ raise RuntimeLogTooSmallError, "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
179
213
  end
180
214
  [test, time]
181
215
  end
182
216
 
183
- if options[:verbose]
184
- puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
185
- end
217
+ puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests" if options[:verbose]
186
218
 
187
- # fill gaps with unknown-runtime if given, average otherwise
188
- known, unknown = tests.partition(&:last)
189
- average = (known.any? ? known.map!(&:last).inject(:+) / known.size : 1)
190
- unknown_runtime = options[:unknown_runtime] || average
191
- unknown.each { |set| set[1] = unknown_runtime }
219
+ set_unknown_runtime tests, options
192
220
  end
193
221
 
194
222
  def runtimes(tests, options)
@@ -196,7 +224,7 @@ module ParallelTests
196
224
  lines = File.read(log).split("\n")
197
225
  lines.each_with_object({}) do |line, times|
198
226
  test, _, time = line.rpartition(':')
199
- next unless test and time
227
+ next unless test && time
200
228
  times[test] = time.to_f if tests.include?(test)
201
229
  end
202
230
  end
@@ -207,17 +235,23 @@ module ParallelTests
207
235
  end
208
236
 
209
237
  def find_tests(tests, options = {})
210
- (tests || []).map do |file_or_folder|
238
+ suffix_pattern = options[:suffix] || test_suffix
239
+ include_pattern = options[:pattern] || //
240
+ exclude_pattern = options[:exclude_pattern]
241
+
242
+ (tests || []).flat_map do |file_or_folder|
211
243
  if File.directory?(file_or_folder)
212
244
  files = files_in_folder(file_or_folder, options)
213
- files.grep(options[:suffix]||test_suffix).grep(options[:pattern]||//)
245
+ files = files.grep(suffix_pattern).grep(include_pattern)
246
+ files -= files.grep(exclude_pattern) if exclude_pattern
247
+ files
214
248
  else
215
249
  file_or_folder
216
250
  end
217
- end.flatten.uniq
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,38 @@ 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
278
+ end
279
+
280
+ private
281
+
282
+ # fill gaps with unknown-runtime if given, average otherwise
283
+ # NOTE: an optimization could be doing runtime by average runtime per file size, but would need file checks
284
+ def set_unknown_runtime(tests, options)
285
+ known, unknown = tests.partition(&:last)
286
+ return if unknown.empty?
287
+ unknown_runtime = options[:unknown_runtime] ||
288
+ (known.empty? ? 1 : known.map!(&:last).sum / known.size) # average
289
+ unknown.each { |set| set[1] = unknown_runtime }
290
+ end
291
+
292
+ def report_process_command?(options)
293
+ options[:verbose] || options[:verbose_command]
229
294
  end
230
295
  end
231
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).inject(:+)
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 { |method| method =~ /^test_/ }
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,56 +75,26 @@ end
74
75
 
75
76
  if defined?(Minitest::Runnable) # Minitest 5
76
77
  class << Minitest::Runnable
77
- prepend(Module.new do
78
- def run(*)
79
- ParallelTests::Test::RuntimeLogger.log_test_run(self) do
80
- super
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
- end)
86
+ )
84
87
  end
85
88
 
86
89
  class << Minitest
87
- prepend(Module.new do
88
- def run(*args)
89
- result = super
90
- ParallelTests::Test::RuntimeLogger.unique_log
91
- result
92
- end
93
- end)
94
- end
95
- elsif defined?(MiniTest::Unit) # Minitest 4
96
- MiniTest::Unit.class_eval do
97
- alias_method :_run_suite_without_runtime_log, :_run_suite
98
- def _run_suite(*args)
99
- ParallelTests::Test::RuntimeLogger.log_test_run(args.first) do
100
- _run_suite_without_runtime_log(*args)
101
- end
102
- end
103
-
104
- alias_method :_run_suites_without_runtime_log, :_run_suites
105
- def _run_suites(*args)
106
- result = _run_suites_without_runtime_log(*args)
107
- ParallelTests::Test::RuntimeLogger.unique_log
108
- result
109
- end
110
- end
111
- else # Test::Unit
112
- require 'test/unit/testsuite'
113
- class ::Test::Unit::TestSuite
114
- alias_method :run_without_timing, :run
115
-
116
- def run(result, &block)
117
- test = tests.first
118
-
119
- if test.is_a? ::Test::Unit::TestSuite # all tests ?
120
- run_without_timing(result, &block)
121
- ParallelTests::Test::RuntimeLogger.unique_log
122
- else
123
- ParallelTests::Test::RuntimeLogger.log_test_run(test.class) do
124
- run_without_timing(result, &block)
90
+ prepend(
91
+ Module.new do
92
+ def run(*args)
93
+ result = super
94
+ ParallelTests::Test::RuntimeLogger.unique_log
95
+ result
125
96
  end
126
97
  end
127
- end
98
+ )
128
99
  end
129
100
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
- VERSION = Version = '2.21.3'
3
+ VERSION = '4.2.1'
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel"
2
3
  require "parallel_tests/railtie" if defined? Rails::Railtie
3
4
  require "rbconfig"
@@ -17,21 +18,19 @@ module ParallelTests
17
18
  count,
18
19
  ENV["PARALLEL_TEST_PROCESSORS"],
19
20
  Parallel.processor_count
20
- ].detect{|c| not c.to_s.strip.empty? }.to_i
21
+ ].detect { |c| !c.to_s.strip.empty? }.to_i
21
22
  end
22
23
 
23
24
  def with_pid_file
24
25
  Tempfile.open('parallel_tests-pidfile') do |f|
25
- begin
26
- ENV['PARALLEL_PID_FILE'] = f.path
27
- # Pids object should be created before threads will start adding pids to it
28
- # Otherwise we would have to use Mutex to prevent creation of several instances
29
- @pids = pids
30
- yield
31
- ensure
32
- ENV['PARALLEL_PID_FILE'] = nil
33
- @pids = nil
34
- end
26
+ ENV['PARALLEL_PID_FILE'] = f.path
27
+ # Pids object should be created before threads will start adding pids to it
28
+ # Otherwise we would have to use Mutex to prevent creation of several instances
29
+ @pids = pids
30
+ yield
31
+ ensure
32
+ ENV['PARALLEL_PID_FILE'] = nil
33
+ @pids = nil
35
34
  end
36
35
  end
37
36
 
@@ -57,7 +56,8 @@ module ParallelTests
57
56
  until !File.directory?(current) || current == previous
58
57
  filename = File.join(current, "Gemfile")
59
58
  return true if File.exist?(filename)
60
- current, previous = File.expand_path("..", current), current
59
+ previous = current
60
+ current = File.expand_path("..", current)
61
61
  end
62
62
 
63
63
  false
@@ -76,7 +76,7 @@ module ParallelTests
76
76
  end
77
77
 
78
78
  def with_ruby_binary(command)
79
- WINDOWS ? "#{RUBY_BINARY} -- #{command}" : command
79
+ WINDOWS ? [RUBY_BINARY, '--', command] : [command]
80
80
  end
81
81
 
82
82
  def wait_for_other_processes_to_finish
@@ -88,13 +88,8 @@ module ParallelTests
88
88
  pids.count
89
89
  end
90
90
 
91
- # real time even if someone messed with timecop in tests
92
91
  def now
93
- if Time.respond_to?(:now_without_mock_time) # Timecop
94
- Time.now_without_mock_time
95
- else
96
- Time.now
97
- end
92
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
98
93
  end
99
94
 
100
95
  def delta
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.21.3
4
+ version: 4.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-22 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -24,7 +24,7 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- description:
27
+ description:
28
28
  email: michael@grosser.it
29
29
  executables:
30
30
  - parallel_spinach
@@ -42,6 +42,7 @@ files:
42
42
  - lib/parallel_tests.rb
43
43
  - lib/parallel_tests/cli.rb
44
44
  - lib/parallel_tests/cucumber/failures_logger.rb
45
+ - lib/parallel_tests/cucumber/features_with_steps.rb
45
46
  - lib/parallel_tests/cucumber/runner.rb
46
47
  - lib/parallel_tests/cucumber/scenario_line_logger.rb
47
48
  - lib/parallel_tests/cucumber/scenarios.rb
@@ -62,11 +63,15 @@ files:
62
63
  - lib/parallel_tests/test/runner.rb
63
64
  - lib/parallel_tests/test/runtime_logger.rb
64
65
  - lib/parallel_tests/version.rb
65
- homepage: http://github.com/grosser/parallel_tests
66
+ homepage: https://github.com/grosser/parallel_tests
66
67
  licenses:
67
68
  - MIT
68
- metadata: {}
69
- post_install_message:
69
+ metadata:
70
+ bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v4.2.1/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v4.2.1
73
+ wiki_uri: https://github.com/grosser/parallel_tests/wiki
74
+ post_install_message:
70
75
  rdoc_options: []
71
76
  require_paths:
72
77
  - lib
@@ -74,16 +79,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
79
  requirements:
75
80
  - - ">="
76
81
  - !ruby/object:Gem::Version
77
- version: 2.0.0
82
+ version: 2.7.0
78
83
  required_rubygems_version: !ruby/object:Gem::Requirement
79
84
  requirements:
80
85
  - - ">="
81
86
  - !ruby/object:Gem::Version
82
87
  version: '0'
83
88
  requirements: []
84
- rubyforge_project:
85
- rubygems_version: 2.7.3
86
- signing_key:
89
+ rubygems_version: 3.3.3
90
+ signing_key:
87
91
  specification_version: 4
88
92
  summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel
89
93
  test_files: []