parallel_tests 3.7.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.
- checksums.yaml +4 -4
- data/Readme.md +10 -9
- data/lib/parallel_tests/cli.rb +38 -31
- data/lib/parallel_tests/cucumber/runner.rb +2 -2
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +1 -1
- data/lib/parallel_tests/cucumber/scenarios.rb +4 -3
- data/lib/parallel_tests/gherkin/runner.rb +11 -16
- data/lib/parallel_tests/grouper.rb +1 -5
- data/lib/parallel_tests/pids.rb +2 -2
- data/lib/parallel_tests/rspec/runner.rb +7 -14
- data/lib/parallel_tests/tasks.rb +105 -60
- data/lib/parallel_tests/test/runner.rb +55 -15
- data/lib/parallel_tests/version.rb +1 -1
- data/lib/parallel_tests.rb +1 -1
- metadata +10 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9044c95c595a48f89a621563c609a4c9fa89d851792ca694a30fcaf947a2119
|
4
|
+
data.tar.gz: 248e38e467b9070c1666819e2c1dc9149b791644b42e8a90cd53df82ebb80eed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 363e8d317a43d03043a270c19d05f60071bd05a9b435a0545454e81b96d875b694dd4fadc74669ba136990d57d863883132a19ff7fd1c9c4169e2c16a8e2db38
|
7
|
+
data.tar.gz: ef251fa00adf1310c5281ee4985119ce6b4810d121d87a3a4a7f6b6a03aa968ed9be7847eec127e74a118983e4454b181635d1538b1a17b5f242fbb11839caa9
|
data/Readme.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# parallel_tests
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/parallel_tests)
|
4
|
-
[](https://github.com/grosser/parallel_tests/actions?query=workflow%3Awindows)
|
4
|
+
[](https://github.com/grosser/parallel_tests/actions?query=workflow%3Atest)
|
6
5
|
|
7
6
|
Speedup Test::Unit + RSpec + Cucumber + Spinach by running parallel on multiple CPU cores.<br/>
|
8
7
|
ParallelTests splits tests into even groups (by number of lines or runtime) and runs each group in a single process with its own database.
|
@@ -42,7 +41,7 @@ test:
|
|
42
41
|
|
43
42
|
### Setup environment from scratch (create db and loads schema, useful for CI)
|
44
43
|
rake parallel:setup
|
45
|
-
|
44
|
+
|
46
45
|
### Drop all test databases
|
47
46
|
rake parallel:drop
|
48
47
|
|
@@ -193,15 +192,15 @@ Setup for non-rails
|
|
193
192
|
- use `ENV['TEST_ENV_NUMBER']` inside your tests to select separate db/memcache/etc. (docker compose: expose it)
|
194
193
|
|
195
194
|
- Only run a subset of files / folders:
|
196
|
-
|
195
|
+
|
197
196
|
`parallel_test test/bar test/baz/foo_text.rb`
|
198
197
|
|
199
198
|
- Pass test-options and files via `--`:
|
200
|
-
|
199
|
+
|
201
200
|
`parallel_rspec -- -t acceptance -f progress -- spec/foo_spec.rb spec/acceptance`
|
202
|
-
|
201
|
+
|
203
202
|
- Pass in test options, by using the -o flag (wrap everything in quotes):
|
204
|
-
|
203
|
+
|
205
204
|
`parallel_cucumber -n 2 -o '-p foo_profile --tags @only_this_tag or @only_that_tag --format summary'`
|
206
205
|
|
207
206
|
Options are:
|
@@ -250,8 +249,7 @@ Options are:
|
|
250
249
|
--first-is-1 Use "1" as TEST_ENV_NUMBER to not reuse the default test environment
|
251
250
|
--fail-fast Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported
|
252
251
|
--verbose Print debug output
|
253
|
-
--verbose-
|
254
|
-
--verbose-rerun-command When there are failures, displays the command executed by each process that failed
|
252
|
+
--verbose-command Displays the command that will be executed by each process and when there are failures displays the command executed by each process that failed
|
255
253
|
--quiet Print only tests output
|
256
254
|
-v, --version Show Version
|
257
255
|
-h, --help Show this.
|
@@ -402,6 +400,9 @@ inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rs
|
|
402
400
|
- [Vikram B Kumar](https://github.com/v-kumar)
|
403
401
|
- [Joshua Pinter](https://github.com/joshuapinter)
|
404
402
|
- [Zach Dennis](https://github.com/zdennis)
|
403
|
+
- [Jon Dufresne](https://github.com/jdufresne)
|
404
|
+
- [Eric Kessler](https://github.com/enkessler)
|
405
|
+
- [Adis Osmonov](https://github.com/adis-io)
|
405
406
|
|
406
407
|
[Michael Grosser](http://grosser.it)<br/>
|
407
408
|
michael@grosser.it<br/>
|
data/lib/parallel_tests/cli.rb
CHANGED
@@ -20,7 +20,7 @@ module ParallelTests
|
|
20
20
|
options[:first_is_1] ||= first_is_1?
|
21
21
|
|
22
22
|
if options[:execute]
|
23
|
-
|
23
|
+
execute_command_in_parallel(options[:execute], num_processes, options)
|
24
24
|
else
|
25
25
|
run_tests_in_parallel(num_processes, options)
|
26
26
|
end
|
@@ -32,9 +32,23 @@ module ParallelTests
|
|
32
32
|
@graceful_shutdown_attempted ||= false
|
33
33
|
Kernel.exit if @graceful_shutdown_attempted
|
34
34
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
|
35
|
+
# In a shell, all sub-processes also get an interrupt, so they shut themselves down.
|
36
|
+
# In a background process this does not happen and we need to do it ourselves.
|
37
|
+
# We cannot always send the interrupt since then the sub-processes would get interrupted twice when in foreground
|
38
|
+
# and that messes with interrupt handling.
|
39
|
+
#
|
40
|
+
# (can simulate detached with `(bundle exec parallel_rspec test/a_spec.rb -n 2 &)`)
|
41
|
+
# also the integration test "passes on int signal to child processes" is detached.
|
42
|
+
#
|
43
|
+
# On windows getpgid does not work so we resort to always killing which is the smaller bug.
|
44
|
+
#
|
45
|
+
# The ParallelTests::Pids `synchronize` method can't be called directly from a trap,
|
46
|
+
# using Thread workaround https://github.com/ddollar/foreman/issues/332
|
47
|
+
Thread.new do
|
48
|
+
if Gem.win_platform? || ((child_pid = ParallelTests.pids.all.first) && Process.getpgid(child_pid) != Process.pid)
|
49
|
+
ParallelTests.stop_all_processes
|
50
|
+
end
|
51
|
+
end
|
38
52
|
|
39
53
|
@graceful_shutdown_attempted = true
|
40
54
|
end
|
@@ -61,20 +75,15 @@ module ParallelTests
|
|
61
75
|
groups = @runner.tests_in_groups(options[:files], num_processes, options)
|
62
76
|
groups.reject!(&:empty?)
|
63
77
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
execute_in_parallel(groups_to_run, groups_to_run.size, options) do |group|
|
68
|
-
run_tests(group, groups_to_run.index(group), 1, options)
|
69
|
-
end
|
70
|
-
else
|
71
|
-
report_number_of_tests(groups) unless options[:quiet]
|
72
|
-
|
73
|
-
execute_in_parallel(groups, groups.size, options) do |group|
|
74
|
-
run_tests(group, groups.index(group), num_processes, options)
|
75
|
-
end
|
78
|
+
if options[:only_group]
|
79
|
+
groups = options[:only_group].map { |i| groups[i - 1] }.compact
|
80
|
+
num_processes = 1
|
76
81
|
end
|
77
82
|
|
83
|
+
report_number_of_tests(groups) unless options[:quiet]
|
84
|
+
test_results = execute_in_parallel(groups, groups.size, options) do |group|
|
85
|
+
run_tests(group, groups.index(group), num_processes, options)
|
86
|
+
end
|
78
87
|
report_results(test_results, options) unless options[:quiet]
|
79
88
|
end
|
80
89
|
|
@@ -100,7 +109,7 @@ module ParallelTests
|
|
100
109
|
|
101
110
|
def run_tests(group, process_number, num_processes, options)
|
102
111
|
if group.empty?
|
103
|
-
{ stdout: '', exit_status: 0, command:
|
112
|
+
{ stdout: '', exit_status: 0, command: nil, seed: nil }
|
104
113
|
else
|
105
114
|
@runner.run_tests(group, process_number, num_processes, options)
|
106
115
|
end
|
@@ -136,13 +145,12 @@ module ParallelTests
|
|
136
145
|
failing_sets = test_results.reject { |r| r[:exit_status] == 0 }
|
137
146
|
return if failing_sets.none?
|
138
147
|
|
139
|
-
if options[:verbose] || options[:
|
148
|
+
if options[:verbose] || options[:verbose_command]
|
140
149
|
puts "\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\n"
|
141
150
|
failing_sets.each do |failing_set|
|
142
151
|
command = failing_set[:command]
|
143
|
-
command = command.gsub(/;export [A-Z_]+;/, ' ') # remove ugly export statements
|
144
152
|
command = @runner.command_with_seed(command, failing_set[:seed]) if failing_set[:seed]
|
145
|
-
|
153
|
+
@runner.print_command(command, failing_set[:env] || {})
|
146
154
|
end
|
147
155
|
end
|
148
156
|
end
|
@@ -229,7 +237,7 @@ module ParallelTests
|
|
229
237
|
processes in a specific formation. Commas indicate specs in the same process,
|
230
238
|
pipes indicate specs in a new process. Cannot use with --single, --isolate, or
|
231
239
|
--isolate-n. Ex.
|
232
|
-
$
|
240
|
+
$ parallel_test -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb'
|
233
241
|
Process 1 will contain 1_spec.rb and 2_spec.rb
|
234
242
|
Process 2 will contain 3_spec.rb
|
235
243
|
Process 3 will contain all other specs
|
@@ -238,8 +246,8 @@ module ParallelTests
|
|
238
246
|
|
239
247
|
opts.on("--only-group INT[,INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
|
240
248
|
|
241
|
-
opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |
|
242
|
-
opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = arg
|
249
|
+
opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |arg| options[:execute] = Shellwords.shellsplit(arg) }
|
250
|
+
opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = Shellwords.shellsplit(arg) }
|
243
251
|
opts.on("-t", "--type [TYPE]", "test(default) / rspec / cucumber / spinach") do |type|
|
244
252
|
@runner = load_runner(type)
|
245
253
|
rescue NameError, LoadError => e
|
@@ -267,8 +275,7 @@ module ParallelTests
|
|
267
275
|
opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
|
268
276
|
opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true }
|
269
277
|
opts.on("--verbose", "Print debug output") { options[:verbose] = true }
|
270
|
-
opts.on("--verbose-
|
271
|
-
opts.on("--verbose-rerun-command", "When there are failures, displays the command executed by each process that failed") { options[:verbose_rerun_command] = true }
|
278
|
+
opts.on("--verbose-command", "Displays the command that will be executed by each process and when there are failures displays the command executed by each process that failed") { options[:verbose_command] = true }
|
272
279
|
opts.on("--quiet", "Print only tests output") { options[:quiet] = true }
|
273
280
|
opts.on("-v", "--version", "Show Version") do
|
274
281
|
puts ParallelTests::VERSION
|
@@ -322,20 +329,20 @@ module ParallelTests
|
|
322
329
|
def extract_file_paths(argv)
|
323
330
|
dash_index = argv.rindex("--")
|
324
331
|
file_args_at = (dash_index || -1) + 1
|
325
|
-
[argv[file_args_at
|
332
|
+
[argv[file_args_at..], argv[0...(dash_index || 0)]]
|
326
333
|
end
|
327
334
|
|
328
335
|
def extract_test_options(argv)
|
329
336
|
dash_index = argv.index("--") || -1
|
330
|
-
argv[dash_index + 1
|
337
|
+
argv[dash_index + 1..]
|
331
338
|
end
|
332
339
|
|
333
340
|
def append_test_options(options, argv)
|
334
341
|
new_opts = extract_test_options(argv)
|
335
342
|
return if new_opts.empty?
|
336
343
|
|
337
|
-
|
338
|
-
options[:test_options]
|
344
|
+
options[:test_options] ||= []
|
345
|
+
options[:test_options] += new_opts
|
339
346
|
end
|
340
347
|
|
341
348
|
def load_runner(type)
|
@@ -345,7 +352,7 @@ module ParallelTests
|
|
345
352
|
klass_name.split('::').inject(Object) { |x, y| x.const_get(y) }
|
346
353
|
end
|
347
354
|
|
348
|
-
def
|
355
|
+
def execute_command_in_parallel(command, num_processes, options)
|
349
356
|
runs = if options[:only_group]
|
350
357
|
options[:only_group].map { |g| g - 1 }
|
351
358
|
else
|
@@ -397,7 +404,7 @@ module ParallelTests
|
|
397
404
|
def simulate_output_for_ci(simulate)
|
398
405
|
if simulate
|
399
406
|
progress_indicator = Thread.new do
|
400
|
-
interval = Float(ENV
|
407
|
+
interval = Float(ENV['PARALLEL_TEST_HEARTBEAT_INTERVAL'] || 60)
|
401
408
|
loop do
|
402
409
|
sleep interval
|
403
410
|
print '.'
|
@@ -35,8 +35,8 @@ module ParallelTests
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def command_with_seed(cmd, seed)
|
38
|
-
clean = cmd
|
39
|
-
|
38
|
+
clean = remove_command_arguments(cmd, '--order')
|
39
|
+
[*clean, '--order', "random:#{seed}"]
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
@@ -27,7 +27,7 @@ module ParallelTests
|
|
27
27
|
example_tags = example.tags.map(&:name)
|
28
28
|
example_tags = scenario_tags + example_tags
|
29
29
|
next unless matches_tags?(example_tags)
|
30
|
-
example.rows[1
|
30
|
+
example.rows[1..].each do |row|
|
31
31
|
test_line = row.source_line
|
32
32
|
next if line_numbers.any? && !line_numbers.include?(test_line)
|
33
33
|
|
@@ -4,7 +4,6 @@ require 'cucumber/runtime'
|
|
4
4
|
require 'cucumber'
|
5
5
|
require 'parallel_tests/cucumber/scenario_line_logger'
|
6
6
|
require 'parallel_tests/gherkin/listener'
|
7
|
-
require 'shellwords'
|
8
7
|
|
9
8
|
begin
|
10
9
|
gem "cuke_modeler", "~> 3.0"
|
@@ -20,7 +19,7 @@ module ParallelTests
|
|
20
19
|
def all(files, options = {})
|
21
20
|
# Parse tag expression from given test options and ignore tag pattern. Refer here to understand how new tag expression syntax works - https://github.com/cucumber/cucumber/tree/master/tag-expressions
|
22
21
|
tags = []
|
23
|
-
words = options[:test_options]
|
22
|
+
words = options[:test_options] || []
|
24
23
|
words.each_with_index { |w, i| tags << words[i + 1] if ["-t", "--tags"].include?(w) }
|
25
24
|
if ignore = options[:ignore_tag_pattern]
|
26
25
|
tags << "not (#{ignore})"
|
@@ -53,7 +52,9 @@ module ParallelTests
|
|
53
52
|
feature_tags = feature.tags.map(&:name)
|
54
53
|
|
55
54
|
# We loop on each children of the feature
|
56
|
-
feature.tests
|
55
|
+
test_models = feature.tests
|
56
|
+
test_models += feature.rules.flat_map(&:tests) if feature.respond_to?(:rules) # cuke_modeler >= 3.2 supports rules
|
57
|
+
test_models.each do |test|
|
57
58
|
# It's a scenario, we add it to the scenario_line_logger
|
58
59
|
scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
|
59
60
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "parallel_tests/test/runner"
|
3
|
-
require 'shellwords'
|
4
3
|
|
5
4
|
module ParallelTests
|
6
5
|
module Gherkin
|
@@ -16,17 +15,13 @@ module ParallelTests
|
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
19
|
-
sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
|
20
|
-
|
21
18
|
options[:env] ||= {}
|
22
19
|
options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
|
23
20
|
|
24
|
-
cmd =
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
cucumber_opts(options[:test_options])
|
29
|
-
].compact.reject(&:empty?).join(' ')
|
21
|
+
cmd = executable
|
22
|
+
cmd += runtime_logging if File.directory?(File.dirname(runtime_log))
|
23
|
+
cmd += combined_scenarios
|
24
|
+
cmd += cucumber_opts(options[:test_options])
|
30
25
|
execute_command(cmd, process_number, num_processes, options)
|
31
26
|
end
|
32
27
|
|
@@ -62,22 +57,22 @@ module ParallelTests
|
|
62
57
|
plural = "s" if (word == group) && (number != 1)
|
63
58
|
"#{number} #{word}#{plural}"
|
64
59
|
end
|
65
|
-
"#{sums[0]} (#{sums[1
|
60
|
+
"#{sums[0]} (#{sums[1..].join(", ")})"
|
66
61
|
end.compact.join("\n")
|
67
62
|
end
|
68
63
|
|
69
64
|
def cucumber_opts(given)
|
70
|
-
if given
|
65
|
+
if given&.include?('--profile') || given&.include?('-p')
|
71
66
|
given
|
72
67
|
else
|
73
|
-
[given, profile_from_config]
|
68
|
+
[*given, *profile_from_config]
|
74
69
|
end
|
75
70
|
end
|
76
71
|
|
77
72
|
def profile_from_config
|
78
73
|
# copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
|
79
74
|
config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
|
80
|
-
|
75
|
+
['--profile', 'parallel'] if config && File.read(config) =~ /^parallel:/
|
81
76
|
end
|
82
77
|
|
83
78
|
def tests_in_groups(tests, num_groups, options = {})
|
@@ -91,7 +86,7 @@ module ParallelTests
|
|
91
86
|
end
|
92
87
|
|
93
88
|
def runtime_logging
|
94
|
-
|
89
|
+
['--format', 'ParallelTests::Gherkin::RuntimeLogger', '--out', runtime_log]
|
95
90
|
end
|
96
91
|
|
97
92
|
def runtime_log
|
@@ -102,11 +97,11 @@ module ParallelTests
|
|
102
97
|
if File.exist?("bin/#{name}")
|
103
98
|
ParallelTests.with_ruby_binary("bin/#{name}")
|
104
99
|
elsif ParallelTests.bundler_enabled?
|
105
|
-
"bundle exec
|
100
|
+
["bundle", "exec", name]
|
106
101
|
elsif File.file?("script/#{name}")
|
107
102
|
ParallelTests.with_ruby_binary("script/#{name}")
|
108
103
|
else
|
109
|
-
name.to_s
|
104
|
+
[name.to_s]
|
110
105
|
end
|
111
106
|
end
|
112
107
|
end
|
@@ -26,10 +26,6 @@ module ParallelTests
|
|
26
26
|
|
27
27
|
isolate_count = isolate_count(options)
|
28
28
|
|
29
|
-
if isolate_count >= num_groups
|
30
|
-
raise 'Number of isolated processes must be less than total the number of processes'
|
31
|
-
end
|
32
|
-
|
33
29
|
if isolate_count >= num_groups
|
34
30
|
raise 'Number of isolated processes must be >= total number of processes'
|
35
31
|
end
|
@@ -38,7 +34,7 @@ module ParallelTests
|
|
38
34
|
# add all files that should run in a multiple isolated processes to their own groups
|
39
35
|
group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
|
40
36
|
# group the non-isolated by size
|
41
|
-
group_features_by_size(items_to_group(items), groups[isolate_count
|
37
|
+
group_features_by_size(items_to_group(items), groups[isolate_count..])
|
42
38
|
else
|
43
39
|
# add all files that should run in a single non-isolated process to first group
|
44
40
|
single_items.each { |item, size| add_to_group(groups.first, item, size) }
|
data/lib/parallel_tests/pids.rb
CHANGED
@@ -43,14 +43,14 @@ module ParallelTests
|
|
43
43
|
|
44
44
|
def read
|
45
45
|
sync do
|
46
|
-
contents =
|
46
|
+
contents = File.read(file_path)
|
47
47
|
return if contents.empty?
|
48
48
|
@pids = JSON.parse(contents)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
52
|
def save
|
53
|
-
sync {
|
53
|
+
sync { File.write(file_path, pids.to_json) }
|
54
54
|
end
|
55
55
|
|
56
56
|
def sync(&block)
|
@@ -7,8 +7,7 @@ module ParallelTests
|
|
7
7
|
DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
|
8
8
|
class << self
|
9
9
|
def run_tests(test_files, process_number, num_processes, options)
|
10
|
-
|
11
|
-
cmd = [exe, options[:test_options], color, spec_opts, *test_files].compact.join(" ")
|
10
|
+
cmd = [*executable, *options[:test_options], *color, *spec_opts, *test_files]
|
12
11
|
execute_command(cmd, process_number, num_processes, options)
|
13
12
|
end
|
14
13
|
|
@@ -16,9 +15,9 @@ module ParallelTests
|
|
16
15
|
if File.exist?("bin/rspec")
|
17
16
|
ParallelTests.with_ruby_binary("bin/rspec")
|
18
17
|
elsif ParallelTests.bundler_enabled?
|
19
|
-
"bundle exec rspec"
|
18
|
+
["bundle", "exec", "rspec"]
|
20
19
|
else
|
21
|
-
"rspec"
|
20
|
+
["rspec"]
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -48,8 +47,8 @@ module ParallelTests
|
|
48
47
|
# --order rand:1234
|
49
48
|
# --order random:1234
|
50
49
|
def command_with_seed(cmd, seed)
|
51
|
-
clean = cmd
|
52
|
-
|
50
|
+
clean = remove_command_arguments(cmd, '--seed', '--order')
|
51
|
+
[*clean, '--seed', seed]
|
53
52
|
end
|
54
53
|
|
55
54
|
# Summarize results from threads and colorize results based on failure and pending counts.
|
@@ -71,19 +70,13 @@ module ParallelTests
|
|
71
70
|
|
72
71
|
private
|
73
72
|
|
74
|
-
# so it can be stubbed....
|
75
|
-
def run(cmd)
|
76
|
-
`#{cmd}`
|
77
|
-
end
|
78
|
-
|
79
73
|
def color
|
80
|
-
'--color --tty' if $stdout.tty?
|
74
|
+
['--color', '--tty'] if $stdout.tty?
|
81
75
|
end
|
82
76
|
|
83
77
|
def spec_opts
|
84
78
|
options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect { |f| File.file?(f) }
|
85
|
-
|
86
|
-
"-O #{options_file}"
|
79
|
+
["-O", options_file] if options_file
|
87
80
|
end
|
88
81
|
end
|
89
82
|
end
|
data/lib/parallel_tests/tasks.rb
CHANGED
@@ -9,16 +9,8 @@ module ParallelTests
|
|
9
9
|
'test'
|
10
10
|
end
|
11
11
|
|
12
|
-
def rake_bin
|
13
|
-
# Prevent 'Exec format error' Errno::ENOEXEC on Windows
|
14
|
-
return "rake" if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
15
|
-
binstub_path = File.join('bin', 'rake')
|
16
|
-
return binstub_path if File.exist?(binstub_path)
|
17
|
-
"rake"
|
18
|
-
end
|
19
|
-
|
20
12
|
def load_lib
|
21
|
-
$LOAD_PATH << File.expand_path(
|
13
|
+
$LOAD_PATH << File.expand_path('..', __dir__)
|
22
14
|
require "parallel_tests"
|
23
15
|
end
|
24
16
|
|
@@ -30,12 +22,15 @@ module ParallelTests
|
|
30
22
|
|
31
23
|
def run_in_parallel(cmd, options = {})
|
32
24
|
load_lib
|
33
|
-
|
25
|
+
|
34
26
|
# Using the relative path to find the binary allow to run a specific version of it
|
35
27
|
executable = File.expand_path('../../bin/parallel_test', __dir__)
|
36
|
-
|
37
|
-
command
|
38
|
-
|
28
|
+
command = ParallelTests.with_ruby_binary(executable)
|
29
|
+
command += ['--exec', Shellwords.join(cmd)]
|
30
|
+
command += ['-n', options[:count]] unless options[:count].to_s.empty?
|
31
|
+
command << '--non-parallel' if options[:non_parallel]
|
32
|
+
|
33
|
+
abort unless system(*command)
|
39
34
|
end
|
40
35
|
|
41
36
|
# this is a crazy-complex solution for a very simple problem:
|
@@ -48,16 +43,14 @@ module ParallelTests
|
|
48
43
|
# - pipefail makes pipe fail with exitstatus of first failed command
|
49
44
|
# - pipefail is not supported in (zsh)
|
50
45
|
# - defining a new rake task like silence_schema would force users to load parallel_tests in test env
|
51
|
-
# - do not use ' since run_in_parallel uses them to quote stuff
|
52
46
|
# - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
|
53
47
|
def suppress_output(command, ignore_regex)
|
54
48
|
activate_pipefail = "set -o pipefail"
|
55
|
-
remove_ignored_lines = %{(grep -v
|
49
|
+
remove_ignored_lines = %{(grep -v #{Shellwords.escape(ignore_regex)} || true)}
|
56
50
|
|
57
|
-
if
|
58
|
-
|
59
|
-
|
60
|
-
%{/bin/bash -c '"'"'#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}'"'"'}
|
51
|
+
if system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null")
|
52
|
+
shell_command = "#{activate_pipefail} && (#{Shellwords.shelljoin(command)}) | #{remove_ignored_lines}"
|
53
|
+
['/bin/bash', '-c', shell_command]
|
61
54
|
else
|
62
55
|
command
|
63
56
|
end
|
@@ -90,7 +83,56 @@ module ParallelTests
|
|
90
83
|
options = args.shift
|
91
84
|
pass_through = args.shift
|
92
85
|
|
93
|
-
[num_processes, pattern
|
86
|
+
[num_processes, pattern, options, pass_through]
|
87
|
+
end
|
88
|
+
|
89
|
+
def schema_format_based_on_rails_version
|
90
|
+
if rails_7_or_greater?
|
91
|
+
ActiveRecord.schema_format
|
92
|
+
else
|
93
|
+
ActiveRecord::Base.schema_format
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def schema_type_based_on_rails_version
|
98
|
+
if rails_61_or_greater? || schema_format_based_on_rails_version == :ruby
|
99
|
+
"schema"
|
100
|
+
else
|
101
|
+
"structure"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_run_command(type, args)
|
106
|
+
count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
|
107
|
+
test_framework = {
|
108
|
+
'spec' => 'rspec',
|
109
|
+
'test' => 'test',
|
110
|
+
'features' => 'cucumber',
|
111
|
+
'features-spinach' => 'spinach'
|
112
|
+
}.fetch(type)
|
113
|
+
|
114
|
+
type = 'features' if test_framework == 'spinach'
|
115
|
+
|
116
|
+
# Using the relative path to find the binary allow to run a specific version of it
|
117
|
+
executable = File.expand_path('../../bin/parallel_test', __dir__)
|
118
|
+
executable = ParallelTests.with_ruby_binary(executable)
|
119
|
+
|
120
|
+
command = [*executable, type, '--type', test_framework]
|
121
|
+
command += ['-n', count.to_s] if count
|
122
|
+
command += ['--pattern', pattern] if pattern
|
123
|
+
command += ['--test-options', options] if options
|
124
|
+
command += Shellwords.shellsplit pass_through if pass_through
|
125
|
+
command
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def rails_7_or_greater?
|
131
|
+
Gem::Version.new(Rails.version) >= Gem::Version.new('7.0')
|
132
|
+
end
|
133
|
+
|
134
|
+
def rails_61_or_greater?
|
135
|
+
Gem::Version.new(Rails.version) >= Gem::Version.new('6.1.0')
|
94
136
|
end
|
95
137
|
end
|
96
138
|
end
|
@@ -99,36 +141,38 @@ end
|
|
99
141
|
namespace :parallel do
|
100
142
|
desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
|
101
143
|
task :setup, :count do |_, args|
|
102
|
-
command = "
|
144
|
+
command = [$0, "db:setup", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"]
|
103
145
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
104
146
|
end
|
105
147
|
|
106
148
|
desc "Create test databases via db:create --> parallel:create[num_cpus]"
|
107
149
|
task :create, :count do |_, args|
|
108
150
|
ParallelTests::Tasks.run_in_parallel(
|
109
|
-
"
|
151
|
+
[$0, "db:create", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
152
|
+
args
|
110
153
|
)
|
111
154
|
end
|
112
155
|
|
113
156
|
desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
|
114
157
|
task :drop, :count do |_, args|
|
115
158
|
ParallelTests::Tasks.run_in_parallel(
|
116
|
-
|
117
|
-
|
159
|
+
[
|
160
|
+
$0,
|
161
|
+
"db:drop",
|
162
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
163
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
164
|
+
],
|
165
|
+
args
|
118
166
|
)
|
119
167
|
end
|
120
168
|
|
121
169
|
desc "Update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
122
170
|
task(:prepare, [:count]) do |_, args|
|
123
171
|
ParallelTests::Tasks.check_for_pending_migrations
|
124
|
-
|
172
|
+
|
173
|
+
if defined?(ActiveRecord) && [:ruby, :sql].include?(ParallelTests::Tasks.schema_format_based_on_rails_version)
|
125
174
|
# fast: dump once, load in parallel
|
126
|
-
type =
|
127
|
-
if Gem::Version.new(Rails.version) >= Gem::Version.new('6.1.0')
|
128
|
-
"schema"
|
129
|
-
else
|
130
|
-
ActiveRecord::Base.schema_format == :ruby ? "schema" : "structure"
|
131
|
-
end
|
175
|
+
type = ParallelTests::Tasks.schema_type_based_on_rails_version
|
132
176
|
|
133
177
|
Rake::Task["db:#{type}:dump"].invoke
|
134
178
|
|
@@ -140,7 +184,7 @@ namespace :parallel do
|
|
140
184
|
# slow: dump and load in in serial
|
141
185
|
args = args.to_hash.merge(non_parallel: true) # normal merge returns nil
|
142
186
|
task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
|
143
|
-
ParallelTests::Tasks.run_in_parallel(
|
187
|
+
ParallelTests::Tasks.run_in_parallel([$0, task_name], args)
|
144
188
|
next
|
145
189
|
end
|
146
190
|
end
|
@@ -149,22 +193,29 @@ namespace :parallel do
|
|
149
193
|
desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
150
194
|
task :migrate, :count do |_, args|
|
151
195
|
ParallelTests::Tasks.run_in_parallel(
|
152
|
-
"
|
196
|
+
[$0, "db:migrate", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
197
|
+
args
|
153
198
|
)
|
154
199
|
end
|
155
200
|
|
156
201
|
desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
|
157
202
|
task :rollback, :count do |_, args|
|
158
203
|
ParallelTests::Tasks.run_in_parallel(
|
159
|
-
"
|
204
|
+
[$0, "db:rollback", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
205
|
+
args
|
160
206
|
)
|
161
207
|
end
|
162
208
|
|
163
209
|
# just load the schema (good for integration server <-> no development db)
|
164
210
|
desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
165
211
|
task :load_schema, :count do |_, args|
|
166
|
-
command =
|
167
|
-
|
212
|
+
command = [
|
213
|
+
$0,
|
214
|
+
ParallelTests::Tasks.purge_before_load,
|
215
|
+
"db:schema:load",
|
216
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
217
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
218
|
+
]
|
168
219
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
169
220
|
end
|
170
221
|
|
@@ -173,23 +224,34 @@ namespace :parallel do
|
|
173
224
|
desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]"
|
174
225
|
task :load_structure, :count do |_, args|
|
175
226
|
ParallelTests::Tasks.run_in_parallel(
|
176
|
-
|
177
|
-
|
227
|
+
[
|
228
|
+
$0,
|
229
|
+
ParallelTests::Tasks.purge_before_load,
|
230
|
+
"db:structure:load",
|
231
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
232
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
233
|
+
],
|
234
|
+
args
|
178
235
|
)
|
179
236
|
end
|
180
237
|
|
181
238
|
desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
182
239
|
task :seed, :count do |_, args|
|
183
240
|
ParallelTests::Tasks.run_in_parallel(
|
184
|
-
|
241
|
+
[
|
242
|
+
$0,
|
243
|
+
"db:seed",
|
244
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
245
|
+
],
|
246
|
+
args
|
185
247
|
)
|
186
248
|
end
|
187
249
|
|
188
250
|
desc "Launch given rake command in parallel"
|
189
251
|
task :rake, :command, :count do |_, args|
|
190
252
|
ParallelTests::Tasks.run_in_parallel(
|
191
|
-
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}
|
192
|
-
|
253
|
+
[$0, args.command, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
254
|
+
args
|
193
255
|
)
|
194
256
|
end
|
195
257
|
|
@@ -198,26 +260,9 @@ namespace :parallel do
|
|
198
260
|
task type, [:count, :pattern, :options, :pass_through] do |_t, args|
|
199
261
|
ParallelTests::Tasks.check_for_pending_migrations
|
200
262
|
ParallelTests::Tasks.load_lib
|
263
|
+
command = ParallelTests::Tasks.build_run_command(type, args)
|
201
264
|
|
202
|
-
|
203
|
-
test_framework = {
|
204
|
-
'spec' => 'rspec',
|
205
|
-
'test' => 'test',
|
206
|
-
'features' => 'cucumber',
|
207
|
-
'features-spinach' => 'spinach'
|
208
|
-
}[type]
|
209
|
-
|
210
|
-
type = 'features' if test_framework == 'spinach'
|
211
|
-
# Using the relative path to find the binary allow to run a specific version of it
|
212
|
-
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
213
|
-
|
214
|
-
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
|
215
|
-
"--type #{test_framework} " \
|
216
|
-
"-n #{count} " \
|
217
|
-
"--pattern '#{pattern}' " \
|
218
|
-
"--test-options '#{options}' " \
|
219
|
-
"#{pass_through}"
|
220
|
-
abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
265
|
+
abort unless system(*command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
221
266
|
end
|
222
267
|
end
|
223
268
|
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'shellwords'
|
2
3
|
require 'parallel_tests'
|
3
4
|
|
4
5
|
module ParallelTests
|
5
6
|
module Test
|
6
7
|
class Runner
|
8
|
+
RuntimeLogTooSmallError = Class.new(StandardError)
|
9
|
+
|
7
10
|
class << self
|
8
11
|
# --- usually overwritten by other runners
|
9
12
|
|
@@ -25,7 +28,14 @@ module ParallelTests
|
|
25
28
|
|
26
29
|
def run_tests(test_files, process_number, num_processes, options)
|
27
30
|
require_list = test_files.map { |file| file.gsub(" ", "\\ ") }.join(" ")
|
28
|
-
cmd =
|
31
|
+
cmd = [
|
32
|
+
*executable,
|
33
|
+
'-Itest',
|
34
|
+
'-e',
|
35
|
+
"%w[#{require_list}].each { |f| require %{./\#{f}} }",
|
36
|
+
'--',
|
37
|
+
*options[:test_options]
|
38
|
+
]
|
29
39
|
execute_command(cmd, process_number, num_processes, options)
|
30
40
|
end
|
31
41
|
|
@@ -63,7 +73,7 @@ module ParallelTests
|
|
63
73
|
[]
|
64
74
|
end
|
65
75
|
if runtimes.size * 1.5 > tests.size
|
66
|
-
puts "Using recorded test runtime"
|
76
|
+
puts "Using recorded test runtime" unless options[:quiet]
|
67
77
|
sort_by_runtime(tests, runtimes)
|
68
78
|
else
|
69
79
|
sort_by_filesize(tests)
|
@@ -76,22 +86,33 @@ module ParallelTests
|
|
76
86
|
end
|
77
87
|
|
78
88
|
def execute_command(cmd, process_number, num_processes, options)
|
89
|
+
number = test_env_number(process_number, options).to_s
|
79
90
|
env = (options[:env] || {}).merge(
|
80
|
-
"TEST_ENV_NUMBER" =>
|
91
|
+
"TEST_ENV_NUMBER" => number,
|
81
92
|
"PARALLEL_TEST_GROUPS" => num_processes.to_s,
|
82
93
|
"PARALLEL_PID_FILE" => ParallelTests.pid_file_path
|
83
94
|
)
|
84
|
-
cmd = "nice
|
85
|
-
|
95
|
+
cmd = ["nice", *cmd] if options[:nice]
|
96
|
+
|
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) }
|
86
99
|
|
87
|
-
|
100
|
+
print_command(cmd, env) if report_process_command?(options) && !options[:serialize_stdout]
|
88
101
|
|
89
102
|
execute_command_and_capture_output(env, cmd, options)
|
90
103
|
end
|
91
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
|
+
|
92
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
|
+
|
93
114
|
pid = nil
|
94
|
-
output = IO.popen(env, cmd) do |io|
|
115
|
+
output = IO.popen(env, cmd, popen_options) do |io|
|
95
116
|
pid = io.pid
|
96
117
|
ParallelTests.pids.add(pid)
|
97
118
|
capture_output(io, env, options)
|
@@ -100,9 +121,9 @@ module ParallelTests
|
|
100
121
|
exitstatus = $?.exitstatus
|
101
122
|
seed = output[/seed (\d+)/, 1]
|
102
123
|
|
103
|
-
output =
|
124
|
+
output = "#{Shellwords.shelljoin(cmd)}\n#{output}" if report_process_command?(options) && options[:serialize_stdout]
|
104
125
|
|
105
|
-
{ stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
|
126
|
+
{ env: env, stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
|
106
127
|
end
|
107
128
|
|
108
129
|
def find_results(test_output)
|
@@ -129,18 +150,22 @@ module ParallelTests
|
|
129
150
|
|
130
151
|
# remove old seed and add new seed
|
131
152
|
def command_with_seed(cmd, seed)
|
132
|
-
clean = cmd
|
133
|
-
|
153
|
+
clean = remove_command_arguments(cmd, '--seed')
|
154
|
+
[*clean, '--seed', seed]
|
134
155
|
end
|
135
156
|
|
136
157
|
protected
|
137
158
|
|
138
159
|
def executable
|
139
|
-
ENV['PARALLEL_TESTS_EXECUTABLE']
|
160
|
+
if (executable = ENV['PARALLEL_TESTS_EXECUTABLE'])
|
161
|
+
[executable]
|
162
|
+
else
|
163
|
+
determine_executable
|
164
|
+
end
|
140
165
|
end
|
141
166
|
|
142
167
|
def determine_executable
|
143
|
-
"ruby"
|
168
|
+
["ruby"]
|
144
169
|
end
|
145
170
|
|
146
171
|
def sum_up_results(results)
|
@@ -184,7 +209,7 @@ module ParallelTests
|
|
184
209
|
allowed_missing -= 1 unless time = runtimes[test]
|
185
210
|
if allowed_missing < 0
|
186
211
|
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."
|
212
|
+
raise RuntimeLogTooSmallError, "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
|
188
213
|
end
|
189
214
|
[test, time]
|
190
215
|
end
|
@@ -237,6 +262,21 @@ module ParallelTests
|
|
237
262
|
Dir[File.join(folder, pattern)].uniq.sort
|
238
263
|
end
|
239
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
|
+
|
240
280
|
private
|
241
281
|
|
242
282
|
# fill gaps with unknown-runtime if given, average otherwise
|
@@ -250,7 +290,7 @@ module ParallelTests
|
|
250
290
|
end
|
251
291
|
|
252
292
|
def report_process_command?(options)
|
253
|
-
options[:verbose] || options[:
|
293
|
+
options[:verbose] || options[:verbose_command]
|
254
294
|
end
|
255
295
|
end
|
256
296
|
end
|
data/lib/parallel_tests.rb
CHANGED
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:
|
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:
|
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
|
@@ -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/
|
72
|
-
source_code_uri: https://github.com/grosser/parallel_tests/tree/
|
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
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.
|
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.
|
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: []
|