parallel_tests 1.3.7 → 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.
@@ -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,25 +15,29 @@ 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.sub(" ", "\\ ") }.join(" ")
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
32
31
 
32
+ # ignores other commands runner noise
33
33
  def line_is_result?(line)
34
- line.gsub!(/[.F*]/,'')
35
- line =~ /\d+ failure/
34
+ line =~ /\d+ failure(?!:)/
36
35
  end
37
36
 
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(tests, runtimes(tests, options), options.merge(allowed_missing: 0.5))
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 = runtimes(tests, options) rescue []
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)
@@ -71,49 +77,60 @@ module ParallelTests
71
77
 
72
78
  def execute_command(cmd, process_number, num_processes, options)
73
79
  env = (options[:env] || {}).merge(
74
- "TEST_ENV_NUMBER" => test_env_number(process_number),
75
- "PARALLEL_TEST_GROUPS" => num_processes
80
+ "TEST_ENV_NUMBER" => test_env_number(process_number, options).to_s,
81
+ "PARALLEL_TEST_GROUPS" => num_processes.to_s,
82
+ "PARALLEL_PID_FILE" => ParallelTests.pid_file_path
76
83
  )
77
84
  cmd = "nice #{cmd}" if options[:nice]
78
85
  cmd = "#{cmd} 2>&1" if options[:combine_stderr]
79
- puts cmd if options[:verbose]
80
86
 
81
- execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
82
- end
87
+ puts cmd if report_process_command?(options) && !options[:serialize_stdout]
83
88
 
84
- def execute_command_and_capture_output(env, cmd, silence)
85
- # make processes descriptive / visible in ps -ef
86
- separator = (WINDOWS ? ' & ' : ';')
87
- exports = env.map do |k,v|
88
- if WINDOWS
89
- "(SET \"#{k}=#{v}\")"
90
- else
91
- "#{k}=#{v};export #{k}"
92
- end
93
- end.join(separator)
94
- cmd = "#{exports}#{separator}#{cmd}"
89
+ execute_command_and_capture_output(env, cmd, options)
90
+ end
95
91
 
96
- output = open("|#{cmd}", "r") { |output| capture_output(output, silence) }
92
+ def execute_command_and_capture_output(env, cmd, options)
93
+ pid = nil
94
+ output = IO.popen(env, cmd) do |io|
95
+ pid = io.pid
96
+ ParallelTests.pids.add(pid)
97
+ capture_output(io, env, options)
98
+ end
99
+ ParallelTests.pids.delete(pid) if pid
97
100
  exitstatus = $?.exitstatus
101
+ seed = output[/seed (\d+)/, 1]
102
+
103
+ output = [cmd, output].join("\n") if report_process_command?(options) && options[:serialize_stdout]
98
104
 
99
- {:stdout => output, :exit_status => exitstatus}
105
+ { stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
100
106
  end
101
107
 
102
108
  def find_results(test_output)
103
- test_output.split("\n").map {|line|
104
- line.gsub!(/\e\[\d+m/,'')
109
+ test_output.lines.map do |line|
110
+ line.chomp!
111
+ line.gsub!(/\e\[\d+m/, '') # remove color coding
105
112
  next unless line_is_result?(line)
106
113
  line
107
- }.compact
114
+ end.compact
108
115
  end
109
116
 
110
- def test_env_number(process_number)
111
- process_number == 0 ? '' : process_number + 1
117
+ def test_env_number(process_number, options = {})
118
+ if process_number == 0 && !options[:first_is_1]
119
+ ''
120
+ else
121
+ process_number + 1
122
+ end
112
123
  end
113
124
 
114
125
  def summarize_results(results)
115
126
  sums = sum_up_results(results)
116
- sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
127
+ sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
128
+ end
129
+
130
+ # remove old seed and add new seed
131
+ def command_with_seed(cmd, seed)
132
+ clean = cmd.sub(/\s--seed\s+\d+\b/, '')
133
+ "#{clean} --seed #{seed}"
117
134
  end
118
135
 
119
136
  protected
@@ -127,34 +144,37 @@ module ParallelTests
127
144
  end
128
145
 
129
146
  def sum_up_results(results)
130
- results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
147
+ results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
131
148
  counts = results.scan(/(\d+) (\w+)/)
132
- counts.inject(Hash.new(0)) do |sum, (number, word)|
149
+ counts.each_with_object(Hash.new(0)) do |(number, word), sum|
133
150
  sum[word] += number.to_i
134
- sum
135
151
  end
136
152
  end
137
153
 
138
154
  # read output of the process and print it in chunks
139
- def capture_output(out, silence)
140
- result = ""
141
- loop do
142
- begin
155
+ def capture_output(out, env, options = {})
156
+ result = +""
157
+ begin
158
+ loop do
143
159
  read = out.readpartial(1000000) # read whatever chunk we can get
144
160
  if Encoding.default_internal
145
161
  read = read.force_encoding(Encoding.default_internal)
146
162
  end
147
163
  result << read
148
- unless silence
149
- $stdout.print read
164
+ unless options[:serialize_stdout]
165
+ message = read
166
+ message = "[TEST GROUP #{env['TEST_ENV_NUMBER']}] #{message}" if options[:prefix_output_with_test_env_number]
167
+ $stdout.print message
150
168
  $stdout.flush
151
169
  end
152
170
  end
153
- end rescue EOFError
171
+ rescue EOFError
172
+ nil
173
+ end
154
174
  result
155
175
  end
156
176
 
157
- def sort_by_runtime(tests, runtimes, options={})
177
+ def sort_by_runtime(tests, runtimes, options = {})
158
178
  allowed_missing = options[:allowed_missing] || 1.0
159
179
  allowed_missing = tests.size * allowed_missing
160
180
 
@@ -162,26 +182,24 @@ module ParallelTests
162
182
  tests.sort!
163
183
  tests.map! do |test|
164
184
  allowed_missing -= 1 unless time = runtimes[test]
165
- raise "Too little runtime info" if allowed_missing < 0
185
+ if allowed_missing < 0
186
+ log = options[:runtime_log] || runtime_log
187
+ raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
188
+ end
166
189
  [test, time]
167
190
  end
168
191
 
169
- if options[:verbose]
170
- puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
171
- end
192
+ puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests" if options[:verbose]
172
193
 
173
- # fill gaps with average runtime
174
- known, unknown = tests.partition(&:last)
175
- average = (known.any? ? known.map!(&:last).inject(:+) / known.size : 1)
176
- unknown.each { |set| set[1] = average }
194
+ set_unknown_runtime tests, options
177
195
  end
178
196
 
179
197
  def runtimes(tests, options)
180
198
  log = options[:runtime_log] || runtime_log
181
199
  lines = File.read(log).split("\n")
182
200
  lines.each_with_object({}) do |line, times|
183
- test, time = line.split(":", 2)
184
- next unless test and time
201
+ test, _, time = line.rpartition(':')
202
+ next unless test && time
185
203
  times[test] = time.to_f if tests.include?(test)
186
204
  end
187
205
  end
@@ -192,17 +210,23 @@ module ParallelTests
192
210
  end
193
211
 
194
212
  def find_tests(tests, options = {})
195
- (tests || []).map do |file_or_folder|
213
+ suffix_pattern = options[:suffix] || test_suffix
214
+ include_pattern = options[:pattern] || //
215
+ exclude_pattern = options[:exclude_pattern]
216
+
217
+ (tests || []).flat_map do |file_or_folder|
196
218
  if File.directory?(file_or_folder)
197
219
  files = files_in_folder(file_or_folder, options)
198
- files.grep(test_suffix).grep(options[:pattern]||//)
220
+ files = files.grep(suffix_pattern).grep(include_pattern)
221
+ files -= files.grep(exclude_pattern) if exclude_pattern
222
+ files
199
223
  else
200
224
  file_or_folder
201
225
  end
202
- end.flatten.uniq
226
+ end.uniq
203
227
  end
204
228
 
205
- def files_in_folder(folder, options={})
229
+ def files_in_folder(folder, options = {})
206
230
  pattern = if options[:symlinks] == false # not nil or true
207
231
  "**/*"
208
232
  else
@@ -210,7 +234,23 @@ module ParallelTests
210
234
  # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
211
235
  "**{,/*/**}/*"
212
236
  end
213
- 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]
214
254
  end
215
255
  end
216
256
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'parallel_tests'
2
3
  require 'parallel_tests/test/runner'
3
4
 
@@ -18,19 +19,28 @@ module ParallelTests
18
19
  end
19
20
 
20
21
  def unique_log
21
- lock do
22
+ with_locked_log do |logfile|
22
23
  separator = "\n"
23
- groups = File.read(logfile).split(separator).map { |line| line.split(":") }.group_by(&:first)
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
- File.write(logfile, lines.join(separator) + separator)
29
+ logfile.rewind
30
+ logfile.write(lines.join(separator) + separator)
31
+ logfile.truncate(logfile.pos)
29
32
  end
30
33
  end
31
34
 
32
35
  private
33
36
 
37
+ def with_locked_log
38
+ File.open(logfile, File::RDWR | File::CREAT) do |logfile|
39
+ logfile.flock(File::LOCK_EX)
40
+ yield logfile
41
+ end
42
+ end
43
+
34
44
  # ensure folder exists + clean out previous log
35
45
  # this will happen in multiple processes, but should be roughly at the same time
36
46
  # so there should be no log message lost
@@ -43,28 +53,18 @@ module ParallelTests
43
53
 
44
54
  def log(test, time)
45
55
  return unless message = message(test, time)
46
- lock do
47
- File.open(logfile, 'a') { |f| f.puts message }
56
+ with_locked_log do |logfile|
57
+ logfile.seek(0, IO::SEEK_END)
58
+ logfile.puts message
48
59
  end
49
60
  end
50
61
 
51
62
  def message(test, delta)
52
- 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_/ }
53
64
  filename = test.instance_method(method).source_location.first.sub("#{Dir.pwd}/", "")
54
65
  "#{filename}:#{delta}"
55
66
  end
56
67
 
57
- def lock
58
- File.open(logfile, 'r') do |f|
59
- begin
60
- f.flock File::LOCK_EX
61
- yield
62
- ensure
63
- f.flock File::LOCK_UN
64
- end
65
- end
66
- end
67
-
68
68
  def logfile
69
69
  ParallelTests::Test::Runner.runtime_log
70
70
  end
@@ -75,54 +75,26 @@ end
75
75
 
76
76
  if defined?(Minitest::Runnable) # Minitest 5
77
77
  class << Minitest::Runnable
78
- alias_method :run_without_runtime_log, :run
79
- def run(*args)
80
- ParallelTests::Test::RuntimeLogger.log_test_run(self) do
81
- run_without_runtime_log(*args)
78
+ prepend(
79
+ Module.new do
80
+ def run(*)
81
+ ParallelTests::Test::RuntimeLogger.log_test_run(self) do
82
+ super
83
+ end
84
+ end
82
85
  end
83
- end
86
+ )
84
87
  end
85
88
 
86
89
  class << Minitest
87
- alias_method :run_without_runtime_log, :run
88
- def run(*args)
89
- result = run_without_runtime_log(*args)
90
- ParallelTests::Test::RuntimeLogger.unique_log
91
- result
92
- end
93
- end
94
- elsif defined?(MiniTest::Unit) # Minitest 4
95
- MiniTest::Unit.class_eval do
96
- alias_method :_run_suite_without_runtime_log, :_run_suite
97
- def _run_suite(*args)
98
- ParallelTests::Test::RuntimeLogger.log_test_run(args.first) do
99
- _run_suite_without_runtime_log(*args)
100
- end
101
- end
102
-
103
- alias_method :_run_suites_without_runtime_log, :_run_suites
104
- def _run_suites(*args)
105
- result = _run_suites_without_runtime_log(*args)
106
- ParallelTests::Test::RuntimeLogger.unique_log
107
- result
108
- end
109
- end
110
- else # Test::Unit
111
- require 'test/unit/testsuite'
112
- class ::Test::Unit::TestSuite
113
- alias_method :run_without_timing, :run
114
-
115
- def run(result, &block)
116
- test = tests.first
117
-
118
- if test.is_a? ::Test::Unit::TestSuite # all tests ?
119
- run_without_timing(result, &block)
120
- ParallelTests::Test::RuntimeLogger.unique_log
121
- else
122
- ParallelTests::Test::RuntimeLogger.log_test_run(test.class) do
123
- 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
124
96
  end
125
97
  end
126
- end
98
+ )
127
99
  end
128
100
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
- VERSION = Version = '1.3.7'
3
+ VERSION = '3.7.3'
3
4
  end
@@ -1,19 +1,16 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel"
2
3
  require "parallel_tests/railtie" if defined? Rails::Railtie
3
4
  require "rbconfig"
4
5
 
5
6
  module ParallelTests
6
7
  WINDOWS = (RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/)
7
- GREP_PROCESSES_COMMAND = \
8
- if WINDOWS
9
- "wmic process get commandline | findstr TEST_ENV_NUMBER | find /c \"TEST_ENV_NUMBER=\" 2>&1"
10
- else
11
- "ps -ef | grep [T]EST_ENV_NUMBER= 2>&1"
12
- end
8
+ RUBY_BINARY = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
13
9
 
14
10
  autoload :CLI, "parallel_tests/cli"
15
11
  autoload :VERSION, "parallel_tests/version"
16
12
  autoload :Grouper, "parallel_tests/grouper"
13
+ autoload :Pids, "parallel_tests/pids"
17
14
 
18
15
  class << self
19
16
  def determine_number_of_processes(count)
@@ -21,7 +18,32 @@ module ParallelTests
21
18
  count,
22
19
  ENV["PARALLEL_TEST_PROCESSORS"],
23
20
  Parallel.processor_count
24
- ].detect{|c| not c.to_s.strip.empty? }.to_i
21
+ ].detect { |c| !c.to_s.strip.empty? }.to_i
22
+ end
23
+
24
+ def with_pid_file
25
+ Tempfile.open('parallel_tests-pidfile') do |f|
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
35
+ end
36
+
37
+ def pids
38
+ @pids ||= Pids.new(pid_file_path)
39
+ end
40
+
41
+ def pid_file_path
42
+ ENV.fetch('PARALLEL_PID_FILE')
43
+ end
44
+
45
+ def stop_all_processes
46
+ pids.all.each { |pid| Process.kill(:INT, pid) }
25
47
  end
26
48
 
27
49
  # copied from http://github.com/carlhuda/bundler Bundler::SharedHelpers#find_gemfile
@@ -34,14 +56,27 @@ module ParallelTests
34
56
  until !File.directory?(current) || current == previous
35
57
  filename = File.join(current, "Gemfile")
36
58
  return true if File.exist?(filename)
37
- current, previous = File.expand_path("..", current), current
59
+ previous = current
60
+ current = File.expand_path("..", current)
38
61
  end
39
62
 
40
63
  false
41
64
  end
42
65
 
43
66
  def first_process?
44
- !ENV["TEST_ENV_NUMBER"] || ENV["TEST_ENV_NUMBER"].to_i == 0
67
+ ENV["TEST_ENV_NUMBER"].to_i <= 1
68
+ end
69
+
70
+ def last_process?
71
+ current_process_number = ENV['TEST_ENV_NUMBER']
72
+ total_processes = ENV['PARALLEL_TEST_GROUPS']
73
+ return true if current_process_number.nil? && total_processes.nil?
74
+ current_process_number = '1' if current_process_number.nil?
75
+ current_process_number == total_processes
76
+ end
77
+
78
+ def with_ruby_binary(command)
79
+ WINDOWS ? "#{RUBY_BINARY} -- #{command}" : command
45
80
  end
46
81
 
47
82
  def wait_for_other_processes_to_finish
@@ -49,20 +84,12 @@ module ParallelTests
49
84
  sleep 1 until number_of_running_processes <= 1
50
85
  end
51
86
 
52
- # Fun fact: this includes the current process if it's run via parallel_tests
53
87
  def number_of_running_processes
54
- result = `#{GREP_PROCESSES_COMMAND}`
55
- raise "Could not grep for processes -> #{result}" if result.strip != "" && !$?.success?
56
- result.split("\n").size
88
+ pids.count
57
89
  end
58
90
 
59
- # real time even if someone messed with timecop in tests
60
91
  def now
61
- if Time.respond_to?(:now_without_mock_time) # Timecop
62
- Time.now_without_mock_time
63
- else
64
- Time.now
65
- end
92
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
93
  end
67
94
 
68
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: 1.3.7
4
+ version: 3.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-12 00:00:00.000000000 Z
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -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
@@ -50,6 +51,7 @@ files:
50
51
  - lib/parallel_tests/gherkin/runner.rb
51
52
  - lib/parallel_tests/gherkin/runtime_logger.rb
52
53
  - lib/parallel_tests/grouper.rb
54
+ - lib/parallel_tests/pids.rb
53
55
  - lib/parallel_tests/railtie.rb
54
56
  - lib/parallel_tests/rspec/failures_logger.rb
55
57
  - lib/parallel_tests/rspec/logger_base.rb
@@ -61,10 +63,14 @@ files:
61
63
  - lib/parallel_tests/test/runner.rb
62
64
  - lib/parallel_tests/test/runtime_logger.rb
63
65
  - lib/parallel_tests/version.rb
64
- homepage: http://github.com/grosser/parallel_tests
66
+ homepage: https://github.com/grosser/parallel_tests
65
67
  licenses:
66
68
  - MIT
67
- metadata: {}
69
+ metadata:
70
+ bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v3.7.3/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v3.7.3
73
+ wiki_uri: https://github.com/grosser/parallel_tests/wiki
68
74
  post_install_message:
69
75
  rdoc_options: []
70
76
  require_paths:
@@ -73,15 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
73
79
  requirements:
74
80
  - - ">="
75
81
  - !ruby/object:Gem::Version
76
- version: 1.9.3
82
+ version: 2.5.0
77
83
  required_rubygems_version: !ruby/object:Gem::Requirement
78
84
  requirements:
79
85
  - - ">="
80
86
  - !ruby/object:Gem::Version
81
87
  version: '0'
82
88
  requirements: []
83
- rubyforge_project:
84
- rubygems_version: 2.2.2
89
+ rubygems_version: 3.2.16
85
90
  signing_key:
86
91
  specification_version: 4
87
92
  summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel