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,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: []