parallel_tests 3.8.1 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +9 -7
- 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 -1
- data/lib/parallel_tests/rspec/runner.rb +7 -14
- data/lib/parallel_tests/tasks.rb +76 -55
- 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: 1bcc6e9cd8a207f7a7ec8253139040265cf1c1962e5d25629bca37e5cd2b111e
|
4
|
+
data.tar.gz: 7db53c69e2048799c12504a6ce7b56b6b7b4833ef77fdc298ba50c61fe8ed742
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f682ef1d3752cd3893e186879d905954456b51900ce0d4cfd2213f1c226d13fb38d1a3f251dc75bccb8da663273f59ef977e0aa92707f3ede49db381a53c40f5
|
7
|
+
data.tar.gz: 7a3bad92876b0b225f381b60b89e7049f84e9069a89b710749ecad0e6beed6a60952ed3d850dbe90621a731a974b7694b9182bb02a0f5e3e5292ccb2b6226614
|
data/Readme.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# parallel_tests
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/parallel_tests.svg)](https://rubygems.org/gems/parallel_tests)
|
4
|
-
[![Build
|
5
|
-
[![Build status](https://github.com/grosser/parallel_tests/workflows/windows/badge.svg)](https://github.com/grosser/parallel_tests/actions?query=workflow%3Awindows)
|
4
|
+
[![Build status](https://github.com/grosser/parallel_tests/workflows/test/badge.svg)](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:
|
@@ -402,6 +401,9 @@ inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rs
|
|
402
401
|
- [Vikram B Kumar](https://github.com/v-kumar)
|
403
402
|
- [Joshua Pinter](https://github.com/joshuapinter)
|
404
403
|
- [Zach Dennis](https://github.com/zdennis)
|
404
|
+
- [Jon Dufresne](https://github.com/jdufresne)
|
405
|
+
- [Eric Kessler](https://github.com/enkessler)
|
406
|
+
- [Adis Osmonov](https://github.com/adis-io)
|
405
407
|
|
406
408
|
[Michael Grosser](http://grosser.it)<br/>
|
407
409
|
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
|
@@ -38,7 +38,7 @@ module ParallelTests
|
|
38
38
|
# add all files that should run in a multiple isolated processes to their own groups
|
39
39
|
group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
|
40
40
|
# group the non-isolated by size
|
41
|
-
group_features_by_size(items_to_group(items), groups[isolate_count
|
41
|
+
group_features_by_size(items_to_group(items), groups[isolate_count..])
|
42
42
|
else
|
43
43
|
# add all files that should run in a single non-isolated process to first group
|
44
44
|
single_items.each { |item, size| add_to_group(groups.first, item, size) }
|
@@ -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,7 @@ 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]
|
94
87
|
end
|
95
88
|
|
96
89
|
def schema_format_based_on_rails_version
|
@@ -109,6 +102,29 @@ module ParallelTests
|
|
109
102
|
end
|
110
103
|
end
|
111
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
|
+
|
112
128
|
private
|
113
129
|
|
114
130
|
def rails_7_or_greater?
|
@@ -125,22 +141,28 @@ end
|
|
125
141
|
namespace :parallel do
|
126
142
|
desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
|
127
143
|
task :setup, :count do |_, args|
|
128
|
-
command = "
|
144
|
+
command = [$0, "db:setup", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"]
|
129
145
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
130
146
|
end
|
131
147
|
|
132
148
|
desc "Create test databases via db:create --> parallel:create[num_cpus]"
|
133
149
|
task :create, :count do |_, args|
|
134
150
|
ParallelTests::Tasks.run_in_parallel(
|
135
|
-
"
|
151
|
+
[$0, "db:create", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
152
|
+
args
|
136
153
|
)
|
137
154
|
end
|
138
155
|
|
139
156
|
desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
|
140
157
|
task :drop, :count do |_, args|
|
141
158
|
ParallelTests::Tasks.run_in_parallel(
|
142
|
-
|
143
|
-
|
159
|
+
[
|
160
|
+
$0,
|
161
|
+
"db:drop",
|
162
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
|
163
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
164
|
+
],
|
165
|
+
args
|
144
166
|
)
|
145
167
|
end
|
146
168
|
|
@@ -162,7 +184,7 @@ namespace :parallel do
|
|
162
184
|
# slow: dump and load in in serial
|
163
185
|
args = args.to_hash.merge(non_parallel: true) # normal merge returns nil
|
164
186
|
task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
|
165
|
-
ParallelTests::Tasks.run_in_parallel(
|
187
|
+
ParallelTests::Tasks.run_in_parallel([$0, task_name], args)
|
166
188
|
next
|
167
189
|
end
|
168
190
|
end
|
@@ -171,23 +193,29 @@ namespace :parallel do
|
|
171
193
|
desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
172
194
|
task :migrate, :count do |_, args|
|
173
195
|
ParallelTests::Tasks.run_in_parallel(
|
174
|
-
"
|
196
|
+
[$0, "db:migrate", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
197
|
+
args
|
175
198
|
)
|
176
199
|
end
|
177
200
|
|
178
201
|
desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
|
179
202
|
task :rollback, :count do |_, args|
|
180
203
|
ParallelTests::Tasks.run_in_parallel(
|
181
|
-
"
|
204
|
+
[$0, "db:rollback", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
205
|
+
args
|
182
206
|
)
|
183
207
|
end
|
184
208
|
|
185
209
|
# just load the schema (good for integration server <-> no development db)
|
186
210
|
desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
187
211
|
task :load_schema, :count do |_, args|
|
188
|
-
command =
|
189
|
-
|
190
|
-
|
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
|
+
]
|
191
219
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
192
220
|
end
|
193
221
|
|
@@ -196,23 +224,34 @@ namespace :parallel do
|
|
196
224
|
desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]"
|
197
225
|
task :load_structure, :count do |_, args|
|
198
226
|
ParallelTests::Tasks.run_in_parallel(
|
199
|
-
|
200
|
-
|
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
|
201
235
|
)
|
202
236
|
end
|
203
237
|
|
204
238
|
desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
205
239
|
task :seed, :count do |_, args|
|
206
240
|
ParallelTests::Tasks.run_in_parallel(
|
207
|
-
|
241
|
+
[
|
242
|
+
$0,
|
243
|
+
"db:seed",
|
244
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
245
|
+
],
|
246
|
+
args
|
208
247
|
)
|
209
248
|
end
|
210
249
|
|
211
250
|
desc "Launch given rake command in parallel"
|
212
251
|
task :rake, :command, :count do |_, args|
|
213
252
|
ParallelTests::Tasks.run_in_parallel(
|
214
|
-
"RAILS_ENV=#{ParallelTests::Tasks.rails_env}
|
215
|
-
|
253
|
+
[$0, args.command, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
|
254
|
+
args
|
216
255
|
)
|
217
256
|
end
|
218
257
|
|
@@ -221,27 +260,9 @@ namespace :parallel do
|
|
221
260
|
task type, [:count, :pattern, :options, :pass_through] do |_t, args|
|
222
261
|
ParallelTests::Tasks.check_for_pending_migrations
|
223
262
|
ParallelTests::Tasks.load_lib
|
263
|
+
command = ParallelTests::Tasks.build_run_command(type, args)
|
224
264
|
|
225
|
-
|
226
|
-
test_framework = {
|
227
|
-
'spec' => 'rspec',
|
228
|
-
'test' => 'test',
|
229
|
-
'features' => 'cucumber',
|
230
|
-
'features-spinach' => 'spinach'
|
231
|
-
}[type]
|
232
|
-
|
233
|
-
type = 'features' if test_framework == 'spinach'
|
234
|
-
# Using the relative path to find the binary allow to run a specific version of it
|
235
|
-
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
236
|
-
|
237
|
-
command =
|
238
|
-
"#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
|
239
|
-
"--type #{test_framework} " \
|
240
|
-
"-n #{count} " \
|
241
|
-
"--pattern '#{pattern}' " \
|
242
|
-
"--test-options '#{options}' " \
|
243
|
-
"#{pass_through}"
|
244
|
-
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
|
245
266
|
end
|
246
267
|
end
|
247
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.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.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:
|
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/
|
72
|
-
source_code_uri: https://github.com/grosser/parallel_tests/tree/
|
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.
|
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.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: []
|