parallel_tests 2.32.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9f48375f06f2320fdda57c5c1340b8aedf1761541d4c802c5a17b6f21cbf858
4
- data.tar.gz: 8e91a9f26aa2710aa023b6d779d7fdf0e768213262d75b7d9969751a3e825089
3
+ metadata.gz: fd8878310c757d2a906bbf5ca62b68e21941cc79472a9833944f88087449c229
4
+ data.tar.gz: d55a5ac186f1c16d9568b8f6bb145e0016bafc1c2fb9748593e96997eaf2034c
5
5
  SHA512:
6
- metadata.gz: 6468e8bbb055db1eaee1a8512cb4226ceeb7acc2ba880d8be30117d863f8b870629088835d2628d28981e9b193a40239646d8a4de8e61467bd0bc48fae50e92e
7
- data.tar.gz: 0dcd9f3aee1aead8329b76e770eb814ef62f465803629fe236d764b6a874eb827458b008aea240689ecadafa02388d49e71291d2c3cca5e74bc24d66137960ce
6
+ metadata.gz: f01821e38506525feaafb8a9ff5a480ab1f6f2b0bed849088d72682a699f82ae50bb2a78b94526f243a65c2474ff8f2d3a06c204b44431a7687d877cbbc91dd8
7
+ data.tar.gz: 2823702fc7e5d047a69c57d3ee4c0b72afbb51603d58e34a3656899d0026a4b749e94494b9edf234fdcc5fd81bae59dfba55f920d777f5f7d1f7c21fb5219552
data/Readme.md CHANGED
@@ -82,6 +82,8 @@ Running things once
82
82
  ===================
83
83
 
84
84
  ```Ruby
85
+ require "parallel_tests"
86
+
85
87
  # preparation:
86
88
  # affected by race-condition: first process may boot slower than the second
87
89
  # either sleep a bit or use a lock for example File.lock
@@ -97,7 +99,6 @@ at_exit do
97
99
  undo_something
98
100
  end
99
101
  end
100
-
101
102
  ```
102
103
 
103
104
  Even test group run-times
@@ -192,7 +193,6 @@ Setup for non-rails
192
193
 
193
194
  Options are:
194
195
  <!-- copy output from bundle exec ./bin/parallel_test -h -->
195
-
196
196
  -n [PROCESSES] How many processes to use, default: available CPUs
197
197
  -p, --pattern [PATTERN] run tests matching this regex pattern
198
198
  --exclude-pattern [PATTERN] exclude tests matching this regex pattern
@@ -205,7 +205,9 @@ Options are:
205
205
  default - runtime when runtime log is filled otherwise filesize
206
206
  -m, --multiply-processes [FLOAT] use given number as a multiplier of processes to run
207
207
  -s, --single [PATTERN] Run all matching files in the same process
208
- -i, --isolate Do not run any other tests in the group used by --single(-s)
208
+ -i, --isolate Do not run any other tests in the group used by --single(-s).
209
+ Automatically turned on if --isolate-n is set above 0.
210
+ --isolate-n Number of processes for isolated groups. Default to 1 when --isolate is on.
209
211
  --only-group INT[, INT]
210
212
  -e, --exec [COMMAND] execute this code parallel and with ENV['TEST_ENV_NUMBER']
211
213
  -o, --test-options '[OPTIONS]' execute test commands with those options
@@ -222,12 +224,14 @@ Options are:
222
224
  --ignore-tags [PATTERN] When counting steps ignore scenarios with tags that match this pattern
223
225
  --nice execute test commands with low priority.
224
226
  --runtime-log [PATH] Location of previously recorded test runtimes
225
- --allowed-missing Allowed percentage of missing runtimes (default = 50)
227
+ --allowed-missing [INT] Allowed percentage of missing runtimes (default = 50)
226
228
  --unknown-runtime [FLOAT] Use given number as unknown runtime (otherwise use average time)
229
+ --first-is-1 Use "1" as TEST_ENV_NUMBER to not reuse the default test environment
230
+ --fail-fast Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported
227
231
  --verbose Print debug output
228
- --verbose-process-command Print the command that will be executed by each process before it begins
229
- --verbose-rerun-command After a process fails, print the command executed by that process
230
- --quiet Print only test output
232
+ --verbose-process-command Displays only the command that will be executed by each process
233
+ --verbose-rerun-command When there are failures, displays the command executed by each process that failed
234
+ --quiet Print only tests output
231
235
  -v, --version Show Version
232
236
  -h, --help Show this.
233
237
 
@@ -257,7 +261,7 @@ TIPS
257
261
  - Use [rspec-retry](https://github.com/NoRedInk/rspec-retry) (not rspec-rerun) to rerun failed tests.
258
262
  - [JUnit formatter configuration](https://github.com/grosser/parallel_tests/wiki#with-rspec_junit_formatter----by-jgarber)
259
263
  - Use [parallel_split_test](https://github.com/grosser/parallel_split_test) to run multiple scenarios in a single spec file, concurrently. (`parallel_tests` [works at the file-level and intends to stay that way](https://github.com/grosser/parallel_tests/issues/747#issuecomment-580216980))
260
-
264
+
261
265
  ### Cucumber
262
266
 
263
267
  - Add a `parallel: foo` profile to your `config/cucumber.yml` and it will be used to run parallel tests
@@ -374,6 +378,7 @@ inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rs
374
378
  - [Calaway](https://github.com/calaway)
375
379
  - [alboyadjian](https://github.com/alboyadjian)
376
380
  - [Nathan Broadbent](https://github.com/ndbroadbent)
381
+ - [Vikram B Kumar](https://github.com/v-kumar)
377
382
 
378
383
  [Michael Grosser](http://grosser.it)<br/>
379
384
  michael@grosser.it<br/>
@@ -42,9 +42,10 @@ module ParallelTests
42
42
  Tempfile.open 'parallel_tests-lock' do |lock|
43
43
  ParallelTests.with_pid_file do
44
44
  simulate_output_for_ci options[:serialize_stdout] do
45
- Parallel.map(items, :in_threads => num_processes) do |item|
45
+ Parallel.map(items, in_threads: num_processes) do |item|
46
46
  result = yield(item)
47
47
  reprint_output(result, lock.path) if options[:serialize_stdout]
48
+ ParallelTests.stop_all_processes if options[:fail_fast] && result[:exit_status] != 0
48
49
  result
49
50
  end
50
51
  end
@@ -191,6 +192,12 @@ module ParallelTests
191
192
  options[:isolate] = true
192
193
  end
193
194
 
195
+ opts.on("--isolate-n [PROCESSES]",
196
+ Integer,
197
+ "Use 'isolate' singles with number of processes, default: 1.") do |n|
198
+ options[:isolate_count] = n
199
+ end
200
+
194
201
  opts.on("--only-group INT[, INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
195
202
 
196
203
  opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |path| options[:execute] = path }
@@ -220,6 +227,7 @@ module ParallelTests
220
227
  opts.on("--allowed-missing [INT]", Integer, "Allowed percentage of missing runtimes (default = 50)") { |percent| options[:allowed_missing_percent] = percent }
221
228
  opts.on("--unknown-runtime [FLOAT]", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time }
222
229
  opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
230
+ opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true }
223
231
  opts.on("--verbose", "Print debug output") { options[:verbose] = true }
224
232
  opts.on("--verbose-process-command", "Displays only the command that will be executed by each process") { options[:verbose_process_command] = true }
225
233
  opts.on("--verbose-rerun-command", "When there are failures, displays the command executed by each process that failed") { options[:verbose_rerun_command] = true }
@@ -316,9 +324,8 @@ module ParallelTests
316
324
  end
317
325
 
318
326
  def final_fail_message
319
- fail_message = "#{@runner.name}s Failed"
327
+ fail_message = "Tests Failed"
320
328
  fail_message = "\e[31m#{fail_message}\e[0m" if use_colors?
321
-
322
329
  fail_message
323
330
  end
324
331
 
@@ -0,0 +1,31 @@
1
+ begin
2
+ gem "cuke_modeler", "~> 3.0"
3
+ require 'cuke_modeler'
4
+ rescue LoadError
5
+ raise 'Grouping by number of cucumber steps requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
6
+ end
7
+
8
+ module ParallelTests
9
+ module Cucumber
10
+ class FeaturesWithSteps
11
+ class << self
12
+ def all(tests, options)
13
+ ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
14
+ # format of hash will be FILENAME => NUM_STEPS
15
+ steps_per_file = tests.each_with_object({}) do |file,steps|
16
+ feature = ::CukeModeler::FeatureFile.new(file).feature
17
+
18
+ # skip feature if it matches tag regex
19
+ next if feature.tags.grep(ignore_tag_pattern).any?
20
+
21
+ # count the number of steps in the file
22
+ # will only include a feature if the regex does not match
23
+ all_steps = feature.scenarios.map{|a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
24
+ steps[file] = all_steps.inject(0,:+)
25
+ end
26
+ steps_per_file.sort_by { |_, value| -value }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,37 +1,33 @@
1
- require 'cucumber/tag_expressions/parser'
2
- require 'cucumber/core/gherkin/tag_expression'
3
-
4
1
  module ParallelTests
5
2
  module Cucumber
6
3
  module Formatters
7
4
  class ScenarioLineLogger
8
5
  attr_reader :scenarios
9
6
 
10
- def initialize(tag_expression = ::Cucumber::Core::Gherkin::TagExpression.new([]))
7
+ def initialize(tag_expression = nil)
11
8
  @scenarios = []
12
9
  @tag_expression = tag_expression
13
10
  end
14
11
 
15
12
  def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
16
- scenario_tags = feature_element[:tags].map { |tag| tag[:name] }
13
+ scenario_tags = feature_element.tags.map { |tag| tag.name }
17
14
  scenario_tags = feature_tags + scenario_tags
18
- if feature_element[:examples].nil? # :Scenario
19
- test_line = feature_element[:location][:line]
15
+ if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
16
+ test_line = feature_element.source_line
20
17
 
21
18
  # We don't accept the feature_element if the current tags are not valid
22
- return unless @tag_expression.evaluate(scenario_tags)
19
+ return unless matches_tags?(scenario_tags)
23
20
  # or if it is not at the correct location
24
21
  return if line_numbers.any? && !line_numbers.include?(test_line)
25
22
 
26
- @scenarios << [uri, feature_element[:location][:line]].join(":")
23
+ @scenarios << [uri, feature_element.source_line].join(":")
27
24
  else # :ScenarioOutline
28
- feature_element[:examples].each do |example|
29
- example_tags = example[:tags].map { |tag| tag[:name] }
25
+ feature_element.examples.each do |example|
26
+ example_tags = example.tags.map(&:name)
30
27
  example_tags = scenario_tags + example_tags
31
- next unless @tag_expression.evaluate(example_tags)
32
- rows = example[:tableBody].select { |body| body[:type] == :TableRow }
33
- rows.each do |row|
34
- test_line = row[:location][:line]
28
+ next unless matches_tags?(example_tags)
29
+ example.rows[1..-1].each do |row|
30
+ test_line = row.source_line
35
31
  next if line_numbers.any? && !line_numbers.include?(test_line)
36
32
 
37
33
  @scenarios << [uri, test_line].join(':')
@@ -42,6 +38,12 @@ module ParallelTests
42
38
 
43
39
  def method_missing(*args)
44
40
  end
41
+
42
+ private
43
+
44
+ def matches_tags?(tags)
45
+ @tag_expression.nil? || @tag_expression.evaluate(tags)
46
+ end
45
47
  end
46
48
  end
47
49
  end
@@ -1,12 +1,17 @@
1
1
  require 'cucumber/tag_expressions/parser'
2
- require 'cucumber/core/gherkin/tag_expression'
3
2
  require 'cucumber/runtime'
4
3
  require 'cucumber'
5
4
  require 'parallel_tests/cucumber/scenario_line_logger'
6
5
  require 'parallel_tests/gherkin/listener'
7
- require 'gherkin/errors'
8
6
  require 'shellwords'
9
7
 
8
+ begin
9
+ gem "cuke_modeler", "~> 3.0"
10
+ require 'cuke_modeler'
11
+ rescue LoadError
12
+ raise 'Grouping by individual cucumber scenarios requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
13
+ end
14
+
10
15
  module ParallelTests
11
16
  module Cucumber
12
17
  class Scenarios
@@ -40,32 +45,17 @@ module ParallelTests
40
45
  path, *test_lines = path.split(/:(?=\d+)/)
41
46
  test_lines.map!(&:to_i)
42
47
 
43
- # We encode the file and get the content of it
44
- source = ::Cucumber::Runtime::NormalisedEncodingFile.read(path)
45
48
  # We create a Gherkin document, this will be used to decode the details of each scenario
46
- document = ::Cucumber::Core::Gherkin::Document.new(path, source)
47
-
48
- # We create a parser for the gherkin document
49
- parser = ::Gherkin::Parser.new()
50
- scanner = ::Gherkin::TokenScanner.new(document.body)
51
-
52
- begin
53
- # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
54
- result = parser.parse(scanner)
55
- feature_tags = result[:feature][:tags].map { |tag| tag[:name] }
56
-
57
- # We loop on each children of the feature
58
- result[:feature][:children].each do |feature_element|
59
- # If the type of the child is not a scenario or scenario outline, we continue, we are only interested by the name of the scenario here
60
- next unless /Scenario/.match(feature_element[:type])
49
+ document = ::CukeModeler::FeatureFile.new(path)
50
+ feature = document.feature
61
51
 
62
- # It's a scenario, we add it to the scenario_line_logger
63
- scenario_line_logger.visit_feature_element(document.uri, feature_element, feature_tags, line_numbers: test_lines)
64
- end
52
+ # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
53
+ feature_tags = feature.tags.map(&:name)
65
54
 
66
- rescue StandardError => e
67
- # Exception if the document is no well formated or error in the tags
68
- raise ::Cucumber::Core::Gherkin::ParseError.new("#{document.uri}: #{e.message}")
55
+ # We loop on each children of the feature
56
+ feature.tests.each do |test|
57
+ # It's a scenario, we add it to the scenario_line_logger
58
+ scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
69
59
  end
70
60
  end
71
61
 
@@ -1,5 +1,3 @@
1
- require 'gherkin/parser'
2
-
3
1
  module ParallelTests
4
2
  module Gherkin
5
3
  class Listener
@@ -14,7 +14,7 @@ module ParallelTests
14
14
  end
15
15
 
16
16
  config.on_event :test_case_finished do |event|
17
- @example_times[event.test_case.feature.file] += ParallelTests.now.to_f - @start_at
17
+ @example_times[event.test_case.location.file] += ParallelTests.now.to_f - @start_at
18
18
  end
19
19
 
20
20
  config.on_event :test_run_finished do |_|
@@ -2,7 +2,7 @@ module ParallelTests
2
2
  class Grouper
3
3
  class << self
4
4
  def by_steps(tests, num_groups, options)
5
- features_with_steps = build_features_with_steps(tests, options)
5
+ features_with_steps = group_by_features_with_steps(tests, options)
6
6
  in_even_groups_by_size(features_with_steps, num_groups)
7
7
  end
8
8
 
@@ -15,19 +15,46 @@ module ParallelTests
15
15
  groups = Array.new(num_groups) { {:items => [], :size => 0} }
16
16
 
17
17
  # add all files that should run in a single process to one group
18
- (options[:single_process] || []).each do |pattern|
19
- matched, items = items.partition { |item, _size| item =~ pattern }
20
- matched.each { |item, size| add_to_group(groups.first, item, size) }
18
+ single_process_patterns = options[:single_process] || []
19
+
20
+ single_items, items = items.partition do |item, _size|
21
+ single_process_patterns.any? { |pattern| item =~ pattern }
21
22
  end
22
23
 
23
- groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
24
- group_features_by_size(items_to_group(items), groups_to_fill)
24
+ isolate_count = isolate_count(options)
25
+
26
+ if isolate_count >= num_groups
27
+ raise 'Number of isolated processes must be less than total the number of processes'
28
+ end
29
+
30
+ if isolate_count >= 1
31
+ # add all files that should run in a multiple isolated processes to their own groups
32
+ group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
33
+ # group the non-isolated by size
34
+ group_features_by_size(items_to_group(items), groups[isolate_count..-1])
35
+ else
36
+ # add all files that should run in a single non-isolated process to first group
37
+ single_items.each { |item, size| add_to_group(groups.first, item, size) }
38
+
39
+ # group all by size
40
+ group_features_by_size(items_to_group(items), groups)
41
+ end
25
42
 
26
43
  groups.map! { |g| g[:items].sort }
27
44
  end
28
45
 
29
46
  private
30
47
 
48
+ def isolate_count(options)
49
+ if options[:isolate_count] && options[:isolate_count] > 1
50
+ options[:isolate_count]
51
+ elsif options[:isolate]
52
+ 1
53
+ else
54
+ 0
55
+ end
56
+ end
57
+
31
58
  def largest_first(files)
32
59
  files.sort_by{|_item, size| size }.reverse
33
60
  end
@@ -41,23 +68,9 @@ module ParallelTests
41
68
  group[:size] += size
42
69
  end
43
70
 
44
- def build_features_with_steps(tests, options)
45
- require 'gherkin/parser'
46
- ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
47
- parser = ::Gherkin::Parser.new
48
- # format of hash will be FILENAME => NUM_STEPS
49
- steps_per_file = tests.each_with_object({}) do |file,steps|
50
- feature = parser.parse(File.read(file)).fetch(:feature)
51
-
52
- # skip feature if it matches tag regex
53
- next if feature[:tags].grep(ignore_tag_pattern).any?
54
-
55
- # count the number of steps in the file
56
- # will only include a feature if the regex does not match
57
- all_steps = feature[:children].map{|a| a[:steps].count if a[:tags].grep(ignore_tag_pattern).empty? }.compact
58
- steps[file] = all_steps.inject(0,:+)
59
- end
60
- steps_per_file.sort_by { |_, value| -value }
71
+ def group_by_features_with_steps(tests, options)
72
+ require 'parallel_tests/cucumber/features_with_steps'
73
+ ParallelTests::Cucumber::FeaturesWithSteps.all(tests, options)
61
74
  end
62
75
 
63
76
  def group_by_scenarios(tests, options={})
@@ -4,8 +4,6 @@ module ParallelTests
4
4
  module RSpec
5
5
  class Runner < ParallelTests::Test::Runner
6
6
  DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
7
- NAME = 'RSpec'
8
-
9
7
  class << self
10
8
  def run_tests(test_files, process_number, num_processes, options)
11
9
  exe = executable # expensive, so we cache
@@ -50,6 +48,22 @@ module ParallelTests
50
48
  "#{clean} --seed #{seed}"
51
49
  end
52
50
 
51
+ # Summarize results from threads and colorize results based on failure and pending counts.
52
+ #
53
+ def summarize_results(results)
54
+ text = super
55
+ return text unless $stdout.tty?
56
+ sums = sum_up_results(results)
57
+ color =
58
+ if sums['failure'].positive?
59
+ 31 # red
60
+ elsif sums['pending'].positive?
61
+ 33 # yellow
62
+ else
63
+ 32 # green
64
+ end
65
+ "\e[#{color}m#{text}\e[0m"
66
+ end
53
67
 
54
68
  private
55
69
 
@@ -5,7 +5,7 @@ module ParallelTests
5
5
  module Tasks
6
6
  class << self
7
7
  def rails_env
8
- ENV['RAILS_ENV'] || 'test'
8
+ 'test'
9
9
  end
10
10
 
11
11
  def rake_bin
@@ -3,15 +3,9 @@ require 'parallel_tests'
3
3
  module ParallelTests
4
4
  module Test
5
5
  class Runner
6
- NAME = 'Test'
7
-
8
6
  class << self
9
7
  # --- usually overwritten by other runners
10
8
 
11
- def name
12
- NAME
13
- end
14
-
15
9
  def runtime_log
16
10
  'tmp/parallel_runtime_test.log'
17
11
  end
@@ -179,7 +173,7 @@ module ParallelTests
179
173
  allowed_missing -= 1 unless time = runtimes[test]
180
174
  if allowed_missing < 0
181
175
  log = options[:runtime_log] || runtime_log
182
- raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update it."
176
+ raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
183
177
  end
184
178
  [test, time]
185
179
  end
@@ -1,3 +1,3 @@
1
1
  module ParallelTests
2
- VERSION = Version = '2.32.0'
2
+ VERSION = Version = '3.4.0'
3
3
  end
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.32.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-15 00:00:00.000000000 Z
11
+ date: 2020-11-23 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
@@ -67,8 +68,8 @@ licenses:
67
68
  - MIT
68
69
  metadata:
69
70
  bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
70
- documentation_uri: https://github.com/grosser/parallel_tests/blob/v2.32.0/Readme.md
71
- source_code_uri: https://github.com/grosser/parallel_tests/tree/v2.32.0
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v3.4.0/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v3.4.0
72
73
  wiki_uri: https://github.com/grosser/parallel_tests/wiki
73
74
  post_install_message:
74
75
  rdoc_options: []
@@ -78,14 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
79
  requirements:
79
80
  - - ">="
80
81
  - !ruby/object:Gem::Version
81
- version: 2.2.0
82
+ version: 2.4.0
82
83
  required_rubygems_version: !ruby/object:Gem::Requirement
83
84
  requirements:
84
85
  - - ">="
85
86
  - !ruby/object:Gem::Version
86
87
  version: '0'
87
88
  requirements: []
88
- rubygems_version: 3.0.3
89
+ rubygems_version: 3.1.3
89
90
  signing_key:
90
91
  specification_version: 4
91
92
  summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel