parallel_tests 3.3.0 → 4.2.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.
@@ -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.sub(" ", "\\ ") }.join(" ")
23
- 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
+ ]
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(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
+ )
50
68
  when nil
51
69
  # use recorded test runtime if we got enough data
52
- runtimes = runtimes(tests, options) rescue []
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" => test_env_number(process_number, options).to_s,
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 #{cmd}" if options[:nice]
73
- cmd = "#{cmd} 2>&1" if options[:combine_stderr]
95
+ cmd = ["nice", *cmd] if options[:nice]
74
96
 
75
- puts cmd if report_process_command?(options) && !options[:serialize_stdout]
97
+ # being able to run with for example `-output foo-$TEST_ENV_NUMBER` worked originally and is convenient
98
+ 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 => output, :exit_status => exitstatus, :command => cmd, :seed => seed}
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| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
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.sub(/\s--seed\s+\d+\b/, '')
123
- "#{clean} --seed #{seed}"
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'] || determine_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.inject(Hash.new(0)) do |sum, (number, word)|
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
- loop do
149
- begin
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
- end rescue EOFError
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 and time
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).inject(:+) / known.size) # average
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[:verbose_process_command]
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).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,22 +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
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
- end)
98
+ )
94
99
  end
95
100
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
- VERSION = Version = '3.3.0'
3
+ VERSION = '4.2.0'
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
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: 3.3.0
4
+ version: 4.2.0
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: 2020-09-16 00:00:00.000000000 Z
11
+ date: 2023-02-06 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
@@ -68,10 +68,10 @@ licenses:
68
68
  - MIT
69
69
  metadata:
70
70
  bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
71
- documentation_uri: https://github.com/grosser/parallel_tests/blob/v3.3.0/Readme.md
72
- source_code_uri: https://github.com/grosser/parallel_tests/tree/v3.3.0
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v4.2.0/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v4.2.0
73
73
  wiki_uri: https://github.com/grosser/parallel_tests/wiki
74
- post_install_message:
74
+ post_install_message:
75
75
  rdoc_options: []
76
76
  require_paths:
77
77
  - lib
@@ -79,15 +79,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 2.4.0
82
+ version: 2.7.0
83
83
  required_rubygems_version: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  requirements: []
89
- rubygems_version: 3.1.3
90
- signing_key:
89
+ rubygems_version: 3.3.3
90
+ signing_key:
91
91
  specification_version: 4
92
92
  summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel
93
93
  test_files: []