parallel_tests 2.21.1 → 2.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Readme.md +20 -14
- data/lib/parallel_tests/cli.rb +41 -22
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +3 -2
- data/lib/parallel_tests/cucumber/scenarios.rb +16 -9
- data/lib/parallel_tests/gherkin/listener.rb +1 -0
- data/lib/parallel_tests/gherkin/runtime_logger.rb +12 -12
- data/lib/parallel_tests/pids.rb +1 -1
- data/lib/parallel_tests/rspec/runner.rb +3 -6
- data/lib/parallel_tests/rspec/runtime_logger.rb +0 -1
- data/lib/parallel_tests/tasks.rb +60 -27
- data/lib/parallel_tests/test/runner.rb +35 -10
- data/lib/parallel_tests/test/runtime_logger.rb +0 -34
- data/lib/parallel_tests/version.rb +1 -1
- data/lib/parallel_tests.rb +1 -6
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f9f48375f06f2320fdda57c5c1340b8aedf1761541d4c802c5a17b6f21cbf858
|
4
|
+
data.tar.gz: 8e91a9f26aa2710aa023b6d779d7fdf0e768213262d75b7d9969751a3e825089
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6468e8bbb055db1eaee1a8512cb4226ceeb7acc2ba880d8be30117d863f8b870629088835d2628d28981e9b193a40239646d8a4de8e61467bd0bc48fae50e92e
|
7
|
+
data.tar.gz: 0dcd9f3aee1aead8329b76e770eb814ef62f465803629fe236d764b6a874eb827458b008aea240689ecadafa02388d49e71291d2c3cca5e74bc24d66137960ce
|
data/Readme.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/parallel_tests.svg)](https://rubygems.org/gems/parallel_tests)
|
4
4
|
[![Build Status](https://travis-ci.org/grosser/parallel_tests.svg)](https://travis-ci.org/grosser/parallel_tests/builds)
|
5
|
-
[![Build status](https://
|
5
|
+
[![Build status](https://github.com/grosser/parallel_tests/workflows/windows/badge.svg)](https://github.com/grosser/parallel_tests/actions?query=workflow%3Awindows)
|
6
6
|
|
7
7
|
Speedup Test::Unit + RSpec + Cucumber + Spinach by running parallel on multiple CPU cores.<br/>
|
8
8
|
ParallelTests splits tests into even groups (by number of lines or runtime) and runs each group in a single process with its own database.
|
@@ -113,9 +113,9 @@ Rspec: Add to your `.rspec_parallel` (or `.rspec`) :
|
|
113
113
|
--format progress
|
114
114
|
--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
|
115
115
|
|
116
|
-
To use a custom logfile location (default: `tmp/
|
116
|
+
To use a custom logfile location (default: `tmp/parallel_runtime_rspec.log`), use the CLI: `parallel_test spec -t rspec --runtime-log my.log`
|
117
117
|
|
118
|
-
###
|
118
|
+
### Minitest
|
119
119
|
|
120
120
|
Add to your `test_helper.rb`:
|
121
121
|
```ruby
|
@@ -195,6 +195,7 @@ Options are:
|
|
195
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
|
+
--exclude-pattern [PATTERN] exclude tests matching this regex pattern
|
198
199
|
--group-by [TYPE] group tests by:
|
199
200
|
found - order of finding files
|
200
201
|
steps - number of cucumber/spinach steps
|
@@ -223,7 +224,10 @@ Options are:
|
|
223
224
|
--runtime-log [PATH] Location of previously recorded test runtimes
|
224
225
|
--allowed-missing Allowed percentage of missing runtimes (default = 50)
|
225
226
|
--unknown-runtime [FLOAT] Use given number as unknown runtime (otherwise use average time)
|
226
|
-
--verbose Print
|
227
|
+
--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
|
227
231
|
-v, --version Show Version
|
228
232
|
-h, --help Show this.
|
229
233
|
|
@@ -252,7 +256,8 @@ TIPS
|
|
252
256
|
- Instantly see failures (instead of just a red F) with [rspec-instafail](https://github.com/grosser/rspec-instafail)
|
253
257
|
- Use [rspec-retry](https://github.com/NoRedInk/rspec-retry) (not rspec-rerun) to rerun failed tests.
|
254
258
|
- [JUnit formatter configuration](https://github.com/grosser/parallel_tests/wiki#with-rspec_junit_formatter----by-jgarber)
|
255
|
-
|
259
|
+
- 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
|
+
|
256
261
|
### Cucumber
|
257
262
|
|
258
263
|
- Add a `parallel: foo` profile to your `config/cucumber.yml` and it will be used to run parallel tests
|
@@ -262,14 +267,13 @@ TIPS
|
|
262
267
|
- Builds a HTML report from JSON with support for debug msgs & embedded Base64 images.
|
263
268
|
|
264
269
|
### General
|
265
|
-
- [SQL schema format] use :ruby schema format to get faster parallel:prepare`
|
266
270
|
- [ZSH] use quotes to use rake arguments `rake "parallel:prepare[3]"`
|
267
271
|
- [Memcached] use different namespaces<br/>
|
268
272
|
e.g. `config.cache_store = ..., namespace: "test_#{ENV['TEST_ENV_NUMBER']}"`
|
269
273
|
- Debug errors that only happen with multiple files using `--verbose` and [cleanser](https://github.com/grosser/cleanser)
|
270
274
|
- `export PARALLEL_TEST_PROCESSORS=13` to override default processor count
|
271
275
|
- Shell alias: `alias prspec='parallel_rspec -m 2 --'`
|
272
|
-
- [Spring]
|
276
|
+
- [Spring] Add the [spring-commands-parallel-tests](https://github.com/DocSpring/spring-commands-parallel-tests) gem to your `Gemfile` to get `parallel_tests` working with Spring.
|
273
277
|
- `--first-is-1` will make the first environment be `1`, so you can test while running your full suite.<br/>
|
274
278
|
`export PARALLEL_TEST_FIRST_IS_1=true` will provide the same result
|
275
279
|
- [email_spec and/or action_mailer_cache_delivery](https://github.com/grosser/parallel_tests/wiki)
|
@@ -279,13 +283,7 @@ TIPS
|
|
279
283
|
- [Sphinx setup](https://github.com/grosser/parallel_tests/wiki)
|
280
284
|
- [Capistrano setup](https://github.com/grosser/parallel_tests/wiki/Remotely-with-capistrano) let your tests run on a big box instead of your laptop
|
281
285
|
|
282
|
-
Contribute your own
|
283
|
-
|
284
|
-
TODO
|
285
|
-
====
|
286
|
-
- fix tests vs cucumber >= 1.2 `unknown option --format`
|
287
|
-
- add unit tests for cucumber runtime formatter
|
288
|
-
- fix windows bugs / get windows CI green
|
286
|
+
Contribute your own gotchas to the [Wiki](https://github.com/grosser/parallel_tests/wiki) or even better open a PR :)
|
289
287
|
|
290
288
|
Authors
|
291
289
|
====
|
@@ -368,6 +366,14 @@ inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rs
|
|
368
366
|
- [Jerry](https://github.com/boblington)
|
369
367
|
- [Aleksei Gusev](https://github.com/hron)
|
370
368
|
- [Scott Olsen](https://github.com/scottolsen)
|
369
|
+
- [Andrei Botalov](https://github.com/abotalov)
|
370
|
+
- [Zachary Attas](https://github.com/snackattas)
|
371
|
+
- [David Rodríguez](https://github.com/deivid-rodriguez)
|
372
|
+
- [Justin Doody](https://github.com/justindoody)
|
373
|
+
- [Sandeep Singh](https://github.com/sandeepnagra)
|
374
|
+
- [Calaway](https://github.com/calaway)
|
375
|
+
- [alboyadjian](https://github.com/alboyadjian)
|
376
|
+
- [Nathan Broadbent](https://github.com/ndbroadbent)
|
371
377
|
|
372
378
|
[Michael Grosser](http://grosser.it)<br/>
|
373
379
|
michael@grosser.it<br/>
|
data/lib/parallel_tests/cli.rb
CHANGED
@@ -2,6 +2,7 @@ require 'optparse'
|
|
2
2
|
require 'tempfile'
|
3
3
|
require 'parallel_tests'
|
4
4
|
require 'shellwords'
|
5
|
+
require 'pathname'
|
5
6
|
|
6
7
|
module ParallelTests
|
7
8
|
class CLI
|
@@ -40,16 +41,12 @@ module ParallelTests
|
|
40
41
|
def execute_in_parallel(items, num_processes, options)
|
41
42
|
Tempfile.open 'parallel_tests-lock' do |lock|
|
42
43
|
ParallelTests.with_pid_file do
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
progress_indicator.exit
|
49
|
-
puts
|
44
|
+
simulate_output_for_ci options[:serialize_stdout] do
|
45
|
+
Parallel.map(items, :in_threads => num_processes) do |item|
|
46
|
+
result = yield(item)
|
47
|
+
reprint_output(result, lock.path) if options[:serialize_stdout]
|
48
|
+
result
|
50
49
|
end
|
51
|
-
reprint_output(result, lock.path) if options[:serialize_stdout]
|
52
|
-
result
|
53
50
|
end
|
54
51
|
end
|
55
52
|
end
|
@@ -58,25 +55,31 @@ module ParallelTests
|
|
58
55
|
def run_tests_in_parallel(num_processes, options)
|
59
56
|
test_results = nil
|
60
57
|
|
61
|
-
|
58
|
+
run_tests_proc = -> {
|
62
59
|
groups = @runner.tests_in_groups(options[:files], num_processes, options)
|
63
60
|
groups.reject! &:empty?
|
64
61
|
|
65
62
|
test_results = if options[:only_group]
|
66
63
|
groups_to_run = options[:only_group].collect{|i| groups[i - 1]}.compact
|
67
|
-
report_number_of_tests(groups_to_run)
|
64
|
+
report_number_of_tests(groups_to_run) unless options[:quiet]
|
68
65
|
execute_in_parallel(groups_to_run, groups_to_run.size, options) do |group|
|
69
66
|
run_tests(group, groups_to_run.index(group), 1, options)
|
70
67
|
end
|
71
68
|
else
|
72
|
-
report_number_of_tests(groups)
|
69
|
+
report_number_of_tests(groups) unless options[:quiet]
|
73
70
|
|
74
71
|
execute_in_parallel(groups, groups.size, options) do |group|
|
75
72
|
run_tests(group, groups.index(group), num_processes, options)
|
76
73
|
end
|
77
74
|
end
|
78
75
|
|
79
|
-
report_results(test_results, options)
|
76
|
+
report_results(test_results, options) unless options[:quiet]
|
77
|
+
}
|
78
|
+
|
79
|
+
if options[:quiet]
|
80
|
+
run_tests_proc.call
|
81
|
+
else
|
82
|
+
report_time_taken(&run_tests_proc)
|
80
83
|
end
|
81
84
|
|
82
85
|
abort final_fail_message if any_test_failed?(test_results)
|
@@ -92,6 +95,7 @@ module ParallelTests
|
|
92
95
|
|
93
96
|
def reprint_output(result, lockfile)
|
94
97
|
lock(lockfile) do
|
98
|
+
$stdout.puts
|
95
99
|
$stdout.puts result[:stdout]
|
96
100
|
$stdout.flush
|
97
101
|
end
|
@@ -121,7 +125,7 @@ module ParallelTests
|
|
121
125
|
failing_sets = test_results.reject { |r| r[:exit_status] == 0 }
|
122
126
|
return if failing_sets.none?
|
123
127
|
|
124
|
-
if options[:verbose]
|
128
|
+
if options[:verbose] || options[:verbose_rerun_command]
|
125
129
|
puts "\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\n"
|
126
130
|
failing_sets.each do |failing_set|
|
127
131
|
command = failing_set[:command]
|
@@ -161,6 +165,7 @@ module ParallelTests
|
|
161
165
|
BANNER
|
162
166
|
opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n }
|
163
167
|
opts.on("-p", "--pattern [PATTERN]", "run tests matching this regex pattern") { |pattern| options[:pattern] = /#{pattern}/ }
|
168
|
+
opts.on("--exclude-pattern", "--exclude-pattern [PATTERN]", "exclude tests matching this regex pattern") { |pattern| options[:exclude_pattern] = /#{pattern}/ }
|
164
169
|
opts.on("--group-by [TYPE]", <<-TEXT.gsub(/^ /, '')
|
165
170
|
group tests by:
|
166
171
|
found - order of finding files
|
@@ -215,11 +220,18 @@ module ParallelTests
|
|
215
220
|
opts.on("--allowed-missing [INT]", Integer, "Allowed percentage of missing runtimes (default = 50)") { |percent| options[:allowed_missing_percent] = percent }
|
216
221
|
opts.on("--unknown-runtime [FLOAT]", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time }
|
217
222
|
opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
|
218
|
-
opts.on("--verbose", "Print
|
223
|
+
opts.on("--verbose", "Print debug output") { options[:verbose] = true }
|
224
|
+
opts.on("--verbose-process-command", "Displays only the command that will be executed by each process") { options[:verbose_process_command] = true }
|
225
|
+
opts.on("--verbose-rerun-command", "When there are failures, displays the command executed by each process that failed") { options[:verbose_rerun_command] = true }
|
226
|
+
opts.on("--quiet", "Print only tests output") { options[:quiet] = true }
|
219
227
|
opts.on("-v", "--version", "Show Version") { puts ParallelTests::VERSION; exit }
|
220
228
|
opts.on("-h", "--help", "Show this.") { puts opts; exit }
|
221
229
|
end.parse!(argv)
|
222
230
|
|
231
|
+
if options[:verbose] && options[:quiet]
|
232
|
+
raise "Both options are mutually exclusive: verbose & quiet"
|
233
|
+
end
|
234
|
+
|
223
235
|
if options[:count] == 0
|
224
236
|
options.delete(:count)
|
225
237
|
options[:non_parallel] = true
|
@@ -228,7 +240,7 @@ module ParallelTests
|
|
228
240
|
files, remaining = extract_file_paths(argv)
|
229
241
|
unless options[:execute]
|
230
242
|
abort "Pass files or folders to run" unless files.any?
|
231
|
-
options[:files] = files
|
243
|
+
options[:files] = files.map { |file_path| Pathname.new(file_path).cleanpath.to_s }
|
232
244
|
end
|
233
245
|
|
234
246
|
append_test_options(options, remaining)
|
@@ -320,13 +332,20 @@ module ParallelTests
|
|
320
332
|
end
|
321
333
|
|
322
334
|
# CI systems often fail when there is no output for a long time, so simulate some output
|
323
|
-
def simulate_output_for_ci
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
335
|
+
def simulate_output_for_ci(simulate)
|
336
|
+
if simulate
|
337
|
+
progress_indicator = Thread.new do
|
338
|
+
interval = Float(ENV.fetch('PARALLEL_TEST_HEARTBEAT_INTERVAL', 60))
|
339
|
+
loop do
|
340
|
+
sleep interval
|
341
|
+
print '.'
|
342
|
+
end
|
329
343
|
end
|
344
|
+
test_results = yield
|
345
|
+
progress_indicator.exit
|
346
|
+
test_results
|
347
|
+
else
|
348
|
+
yield
|
330
349
|
end
|
331
350
|
end
|
332
351
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'cucumber/tag_expressions/parser'
|
1
2
|
require 'cucumber/core/gherkin/tag_expression'
|
2
3
|
|
3
4
|
module ParallelTests
|
@@ -12,7 +13,7 @@ module ParallelTests
|
|
12
13
|
end
|
13
14
|
|
14
15
|
def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
|
15
|
-
scenario_tags = feature_element[:tags].map {|tag|
|
16
|
+
scenario_tags = feature_element[:tags].map { |tag| tag[:name] }
|
16
17
|
scenario_tags = feature_tags + scenario_tags
|
17
18
|
if feature_element[:examples].nil? # :Scenario
|
18
19
|
test_line = feature_element[:location][:line]
|
@@ -25,7 +26,7 @@ module ParallelTests
|
|
25
26
|
@scenarios << [uri, feature_element[:location][:line]].join(":")
|
26
27
|
else # :ScenarioOutline
|
27
28
|
feature_element[:examples].each do |example|
|
28
|
-
example_tags = example[:tags].map {|tag|
|
29
|
+
example_tags = example[:tags].map { |tag| tag[:name] }
|
29
30
|
example_tags = scenario_tags + example_tags
|
30
31
|
next unless @tag_expression.evaluate(example_tags)
|
31
32
|
rows = example[:tableBody].select { |body| body[:type] == :TableRow }
|
@@ -1,31 +1,38 @@
|
|
1
|
+
require 'cucumber/tag_expressions/parser'
|
1
2
|
require 'cucumber/core/gherkin/tag_expression'
|
2
3
|
require 'cucumber/runtime'
|
3
4
|
require 'cucumber'
|
4
5
|
require 'parallel_tests/cucumber/scenario_line_logger'
|
5
6
|
require 'parallel_tests/gherkin/listener'
|
6
7
|
require 'gherkin/errors'
|
8
|
+
require 'shellwords'
|
7
9
|
|
8
10
|
module ParallelTests
|
9
11
|
module Cucumber
|
10
12
|
class Scenarios
|
11
13
|
class << self
|
12
14
|
def all(files, options={})
|
15
|
+
# 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
|
13
16
|
tags = []
|
14
|
-
|
15
|
-
|
17
|
+
words = options[:test_options].to_s.shellsplit
|
18
|
+
words.each_with_index { |w,i| tags << words[i+1] if ["-t", "--tags"].include?(w) }
|
19
|
+
if ignore = options[:ignore_tag_pattern]
|
20
|
+
tags << "not (#{ignore})"
|
21
|
+
end
|
22
|
+
tags_exp = tags.compact.join(" and ")
|
16
23
|
|
17
|
-
split_into_scenarios files,
|
24
|
+
split_into_scenarios files, tags_exp
|
18
25
|
end
|
19
26
|
|
20
27
|
private
|
21
28
|
|
22
|
-
def split_into_scenarios(files, tags=
|
23
|
-
|
24
|
-
# Create the tag expression instance from gherkin, this is needed to know if the scenario matches with the tags invoked by the request
|
25
|
-
tag_expression = ::Cucumber::Core::Gherkin::TagExpression.new(tags)
|
29
|
+
def split_into_scenarios(files, tags='')
|
26
30
|
|
31
|
+
# Create the tag expression instance from cucumber tag expressions parser, this is needed to know if the scenario matches with the tags invoked by the request
|
27
32
|
# Create the ScenarioLineLogger which will filter the scenario we want
|
28
|
-
|
33
|
+
args = []
|
34
|
+
args << ::Cucumber::TagExpressions::Parser.new.parse(tags) unless tags.empty?
|
35
|
+
scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(*args)
|
29
36
|
|
30
37
|
# here we loop on the files map, each file will contain one or more scenario
|
31
38
|
features ||= files.map do |path|
|
@@ -45,7 +52,7 @@ module ParallelTests
|
|
45
52
|
begin
|
46
53
|
# We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
|
47
54
|
result = parser.parse(scanner)
|
48
|
-
feature_tags = result[:feature][:tags].map { |tag|
|
55
|
+
feature_tags = result[:feature][:tags].map { |tag| tag[:name] }
|
49
56
|
|
50
57
|
# We loop on each children of the feature
|
51
58
|
result[:feature][:children].each do |feature_element|
|
@@ -5,22 +5,22 @@ module ParallelTests
|
|
5
5
|
class RuntimeLogger
|
6
6
|
include Io
|
7
7
|
|
8
|
-
def initialize(
|
9
|
-
@io = prepare_io(
|
8
|
+
def initialize(config)
|
9
|
+
@io = prepare_io(config.out_stream)
|
10
10
|
@example_times = Hash.new(0)
|
11
|
-
end
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
config.on_event :test_case_started do |_|
|
13
|
+
@start_at = ParallelTests.now.to_f
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
config.on_event :test_case_finished do |event|
|
17
|
+
@example_times[event.test_case.feature.file] += ParallelTests.now.to_f - @start_at
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
config.on_event :test_run_finished do |_|
|
21
|
+
lock_output do
|
22
|
+
@io.puts @example_times.map { |file, time| "#{file}:#{time}" }
|
23
|
+
end
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
data/lib/parallel_tests/pids.rb
CHANGED
@@ -14,17 +14,14 @@ module ParallelTests
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def determine_executable
|
17
|
-
|
17
|
+
case
|
18
18
|
when File.exist?("bin/rspec")
|
19
19
|
ParallelTests.with_ruby_binary("bin/rspec")
|
20
20
|
when ParallelTests.bundler_enabled?
|
21
|
-
|
22
|
-
"bundle exec #{cmd}"
|
21
|
+
"bundle exec rspec"
|
23
22
|
else
|
24
|
-
|
23
|
+
"rspec"
|
25
24
|
end
|
26
|
-
|
27
|
-
cmd or raise("Can't find executables rspec or spec")
|
28
25
|
end
|
29
26
|
|
30
27
|
def runtime_log
|
@@ -34,7 +34,6 @@ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
|
|
34
34
|
|
35
35
|
def start_dump(*args)
|
36
36
|
return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
|
37
|
-
# TODO: Figure out why sometimes time can be less than 0
|
38
37
|
lock_output do
|
39
38
|
@example_times.each do |file, time|
|
40
39
|
relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'').sub(/^\.\//, "")
|
data/lib/parallel_tests/tasks.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rake'
|
2
|
+
require 'shellwords'
|
2
3
|
|
3
4
|
module ParallelTests
|
4
5
|
module Tasks
|
@@ -7,13 +8,27 @@ module ParallelTests
|
|
7
8
|
ENV['RAILS_ENV'] || 'test'
|
8
9
|
end
|
9
10
|
|
11
|
+
def rake_bin
|
12
|
+
# Prevent 'Exec format error' Errno::ENOEXEC on Windows
|
13
|
+
return "rake" if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
14
|
+
binstub_path = File.join('bin', 'rake')
|
15
|
+
return binstub_path if File.exist?(binstub_path)
|
16
|
+
"rake"
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_lib
|
20
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
21
|
+
require "parallel_tests"
|
22
|
+
end
|
23
|
+
|
10
24
|
def purge_before_load
|
11
25
|
if Gem::Version.new(Rails.version) > Gem::Version.new('4.2.0')
|
12
|
-
Rake::Task.task_defined?('db:
|
26
|
+
Rake::Task.task_defined?('db:purge') ? 'db:purge' : 'app:db:purge'
|
13
27
|
end
|
14
28
|
end
|
15
29
|
|
16
30
|
def run_in_parallel(cmd, options={})
|
31
|
+
load_lib
|
17
32
|
count = " -n #{options[:count]}" unless options[:count].to_s.empty?
|
18
33
|
# Using the relative path to find the binary allow to run a specific version of it
|
19
34
|
executable = File.expand_path("../../../bin/parallel_test", __FILE__)
|
@@ -59,10 +74,10 @@ module ParallelTests
|
|
59
74
|
end
|
60
75
|
end
|
61
76
|
|
62
|
-
# parallel:spec[:count, :pattern, :options]
|
77
|
+
# parallel:spec[:count, :pattern, :options, :pass_through]
|
63
78
|
def parse_args(args)
|
64
79
|
# order as given by user
|
65
|
-
args = [args[:count], args[:pattern], args[:options]]
|
80
|
+
args = [args[:count], args[:pattern], args[:options], args[:pass_through]]
|
66
81
|
|
67
82
|
# count given or empty ?
|
68
83
|
# parallel:spec[2,models,options]
|
@@ -71,8 +86,9 @@ module ParallelTests
|
|
71
86
|
num_processes = count.to_i unless count.to_s.empty?
|
72
87
|
pattern = args.shift
|
73
88
|
options = args.shift
|
89
|
+
pass_through = args.shift
|
74
90
|
|
75
|
-
[num_processes, pattern.to_s, options.to_s]
|
91
|
+
[num_processes, pattern.to_s, options.to_s, pass_through.to_s]
|
76
92
|
end
|
77
93
|
end
|
78
94
|
end
|
@@ -81,78 +97,93 @@ end
|
|
81
97
|
namespace :parallel do
|
82
98
|
desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
|
83
99
|
task :setup, :count do |_,args|
|
84
|
-
command = "
|
100
|
+
command = "#{ParallelTests::Tasks.rake_bin} db:setup RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
85
101
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
86
102
|
end
|
87
103
|
|
88
104
|
desc "Create test databases via db:create --> parallel:create[num_cpus]"
|
89
105
|
task :create, :count do |_,args|
|
90
|
-
ParallelTests::Tasks.run_in_parallel(
|
106
|
+
ParallelTests::Tasks.run_in_parallel(
|
107
|
+
"#{ParallelTests::Tasks.rake_bin} db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
91
108
|
end
|
92
109
|
|
93
110
|
desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
|
94
111
|
task :drop, :count do |_,args|
|
95
|
-
ParallelTests::Tasks.run_in_parallel(
|
112
|
+
ParallelTests::Tasks.run_in_parallel(
|
113
|
+
"#{ParallelTests::Tasks.rake_bin} db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env} " \
|
114
|
+
"DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args)
|
96
115
|
end
|
97
116
|
|
98
117
|
desc "Update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
99
118
|
task(:prepare, [:count]) do |_,args|
|
100
119
|
ParallelTests::Tasks.check_for_pending_migrations
|
101
|
-
if defined?(ActiveRecord) && ActiveRecord::Base.schema_format
|
102
|
-
# dump
|
103
|
-
|
104
|
-
Rake::Task[
|
120
|
+
if defined?(ActiveRecord::Base) && [:ruby, :sql].include?(ActiveRecord::Base.schema_format)
|
121
|
+
# fast: dump once, load in parallel
|
122
|
+
type = (ActiveRecord::Base.schema_format == :ruby ? "schema" : "structure")
|
123
|
+
Rake::Task["db:#{type}:dump"].invoke
|
124
|
+
|
125
|
+
# remove database connection to prevent "database is being accessed by other users"
|
126
|
+
ActiveRecord::Base.remove_connection if ActiveRecord::Base.configurations.any?
|
127
|
+
|
128
|
+
Rake::Task["parallel:load_#{type}"].invoke(args[:count])
|
105
129
|
else
|
106
|
-
#
|
130
|
+
# slow: dump and load in in serial
|
107
131
|
args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
|
108
|
-
|
109
|
-
ParallelTests::Tasks.run_in_parallel("
|
132
|
+
task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
|
133
|
+
ParallelTests::Tasks.run_in_parallel("#{ParallelTests::Tasks.rake_bin} #{task_name}", args)
|
134
|
+
next
|
110
135
|
end
|
111
136
|
end
|
112
137
|
|
113
138
|
# when dumping/resetting takes too long
|
114
139
|
desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
115
140
|
task :migrate, :count do |_,args|
|
116
|
-
ParallelTests::Tasks.run_in_parallel(
|
141
|
+
ParallelTests::Tasks.run_in_parallel(
|
142
|
+
"#{ParallelTests::Tasks.rake_bin} db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
117
143
|
end
|
118
144
|
|
119
145
|
desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
|
120
146
|
task :rollback, :count do |_,args|
|
121
|
-
ParallelTests::Tasks.run_in_parallel(
|
147
|
+
ParallelTests::Tasks.run_in_parallel(
|
148
|
+
"#{ParallelTests::Tasks.rake_bin} db:rollback RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
122
149
|
end
|
123
150
|
|
124
151
|
# just load the schema (good for integration server <-> no development db)
|
125
152
|
desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
126
153
|
task :load_schema, :count do |_,args|
|
127
|
-
command = "
|
154
|
+
command = "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
|
155
|
+
"db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
|
128
156
|
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
|
129
157
|
end
|
130
158
|
|
131
159
|
# load the structure from the structure.sql file
|
132
160
|
desc "Load structure for test databases via db:structure:load --> parallel:load_structure[num_cpus]"
|
133
161
|
task :load_structure, :count do |_,args|
|
134
|
-
ParallelTests::Tasks.run_in_parallel(
|
162
|
+
ParallelTests::Tasks.run_in_parallel(
|
163
|
+
"#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
|
164
|
+
"db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args)
|
135
165
|
end
|
136
166
|
|
137
167
|
desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
138
168
|
task :seed, :count do |_,args|
|
139
|
-
ParallelTests::Tasks.run_in_parallel(
|
169
|
+
ParallelTests::Tasks.run_in_parallel(
|
170
|
+
"#{ParallelTests::Tasks.rake_bin} db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
140
171
|
end
|
141
172
|
|
142
173
|
desc "Launch given rake command in parallel"
|
143
174
|
task :rake, :command, :count do |_, args|
|
144
|
-
ParallelTests::Tasks.run_in_parallel(
|
175
|
+
ParallelTests::Tasks.run_in_parallel(
|
176
|
+
"RAILS_ENV=#{ParallelTests::Tasks.rails_env} #{ParallelTests::Tasks.rake_bin} " \
|
177
|
+
"#{args.command}", args)
|
145
178
|
end
|
146
179
|
|
147
180
|
['test', 'spec', 'features', 'features-spinach'].each do |type|
|
148
181
|
desc "Run #{type} in parallel with parallel:#{type}[num_cpus]"
|
149
|
-
task type, [:count, :pattern, :options] do |t, args|
|
182
|
+
task type, [:count, :pattern, :options, :pass_through] do |t, args|
|
150
183
|
ParallelTests::Tasks.check_for_pending_migrations
|
184
|
+
ParallelTests::Tasks.load_lib
|
151
185
|
|
152
|
-
|
153
|
-
require "parallel_tests"
|
154
|
-
|
155
|
-
count, pattern, options = ParallelTests::Tasks.parse_args(args)
|
186
|
+
count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
|
156
187
|
test_framework = {
|
157
188
|
'spec' => 'rspec',
|
158
189
|
'test' => 'test',
|
@@ -166,10 +197,12 @@ namespace :parallel do
|
|
166
197
|
# Using the relative path to find the binary allow to run a specific version of it
|
167
198
|
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
168
199
|
|
169
|
-
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type}
|
200
|
+
command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
|
201
|
+
"--type #{test_framework} " \
|
170
202
|
"-n #{count} " \
|
171
203
|
"--pattern '#{pattern}' " \
|
172
|
-
"--test-options '#{options}'"
|
204
|
+
"--test-options '#{options}' " \
|
205
|
+
"#{pass_through}"
|
173
206
|
abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
174
207
|
end
|
175
208
|
end
|
@@ -78,7 +78,7 @@ module ParallelTests
|
|
78
78
|
cmd = "nice #{cmd}" if options[:nice]
|
79
79
|
cmd = "#{cmd} 2>&1" if options[:combine_stderr]
|
80
80
|
|
81
|
-
puts cmd if options[:
|
81
|
+
puts cmd if report_process_command?(options) && !options[:serialize_stdout]
|
82
82
|
|
83
83
|
execute_command_and_capture_output(env, cmd, options)
|
84
84
|
end
|
@@ -94,6 +94,10 @@ module ParallelTests
|
|
94
94
|
exitstatus = $?.exitstatus
|
95
95
|
seed = output[/seed (\d+)/,1]
|
96
96
|
|
97
|
+
if report_process_command?(options) && options[:serialize_stdout]
|
98
|
+
output = [cmd, output].join("\n")
|
99
|
+
end
|
100
|
+
|
97
101
|
{:stdout => output, :exit_status => exitstatus, :command => cmd, :seed => seed}
|
98
102
|
end
|
99
103
|
|
@@ -173,7 +177,10 @@ module ParallelTests
|
|
173
177
|
tests.sort!
|
174
178
|
tests.map! do |test|
|
175
179
|
allowed_missing -= 1 unless time = runtimes[test]
|
176
|
-
|
180
|
+
if allowed_missing < 0
|
181
|
+
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."
|
183
|
+
end
|
177
184
|
[test, time]
|
178
185
|
end
|
179
186
|
|
@@ -181,11 +188,7 @@ module ParallelTests
|
|
181
188
|
puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
|
182
189
|
end
|
183
190
|
|
184
|
-
|
185
|
-
known, unknown = tests.partition(&:last)
|
186
|
-
average = (known.any? ? known.map!(&:last).inject(:+) / known.size : 1)
|
187
|
-
unknown_runtime = options[:unknown_runtime] || average
|
188
|
-
unknown.each { |set| set[1] = unknown_runtime }
|
191
|
+
set_unknown_runtime tests, options
|
189
192
|
end
|
190
193
|
|
191
194
|
def runtimes(tests, options)
|
@@ -204,14 +207,20 @@ module ParallelTests
|
|
204
207
|
end
|
205
208
|
|
206
209
|
def find_tests(tests, options = {})
|
207
|
-
|
210
|
+
suffix_pattern = options[:suffix] || test_suffix
|
211
|
+
include_pattern = options[:pattern] || //
|
212
|
+
exclude_pattern = options[:exclude_pattern]
|
213
|
+
|
214
|
+
(tests || []).flat_map do |file_or_folder|
|
208
215
|
if File.directory?(file_or_folder)
|
209
216
|
files = files_in_folder(file_or_folder, options)
|
210
|
-
files.grep(
|
217
|
+
files = files.grep(suffix_pattern).grep(include_pattern)
|
218
|
+
files -= files.grep(exclude_pattern) if exclude_pattern
|
219
|
+
files
|
211
220
|
else
|
212
221
|
file_or_folder
|
213
222
|
end
|
214
|
-
end.
|
223
|
+
end.uniq
|
215
224
|
end
|
216
225
|
|
217
226
|
def files_in_folder(folder, options={})
|
@@ -224,6 +233,22 @@ module ParallelTests
|
|
224
233
|
end
|
225
234
|
Dir[File.join(folder, pattern)].uniq
|
226
235
|
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
# fill gaps with unknown-runtime if given, average otherwise
|
240
|
+
# NOTE: an optimization could be doing runtime by average runtime per file size, but would need file checks
|
241
|
+
def set_unknown_runtime(tests, options)
|
242
|
+
known, unknown = tests.partition(&:last)
|
243
|
+
return if unknown.empty?
|
244
|
+
unknown_runtime = options[:unknown_runtime] ||
|
245
|
+
(known.empty? ? 1 : known.map!(&:last).inject(:+) / known.size) # average
|
246
|
+
unknown.each { |set| set[1] = unknown_runtime }
|
247
|
+
end
|
248
|
+
|
249
|
+
def report_process_command?(options)
|
250
|
+
options[:verbose] || options[:verbose_process_command]
|
251
|
+
end
|
227
252
|
end
|
228
253
|
end
|
229
254
|
end
|
@@ -92,38 +92,4 @@ if defined?(Minitest::Runnable) # Minitest 5
|
|
92
92
|
end
|
93
93
|
end)
|
94
94
|
end
|
95
|
-
elsif defined?(MiniTest::Unit) # Minitest 4
|
96
|
-
MiniTest::Unit.class_eval do
|
97
|
-
alias_method :_run_suite_without_runtime_log, :_run_suite
|
98
|
-
def _run_suite(*args)
|
99
|
-
ParallelTests::Test::RuntimeLogger.log_test_run(args.first) do
|
100
|
-
_run_suite_without_runtime_log(*args)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
alias_method :_run_suites_without_runtime_log, :_run_suites
|
105
|
-
def _run_suites(*args)
|
106
|
-
result = _run_suites_without_runtime_log(*args)
|
107
|
-
ParallelTests::Test::RuntimeLogger.unique_log
|
108
|
-
result
|
109
|
-
end
|
110
|
-
end
|
111
|
-
else # Test::Unit
|
112
|
-
require 'test/unit/testsuite'
|
113
|
-
class ::Test::Unit::TestSuite
|
114
|
-
alias_method :run_without_timing, :run
|
115
|
-
|
116
|
-
def run(result, &block)
|
117
|
-
test = tests.first
|
118
|
-
|
119
|
-
if test.is_a? ::Test::Unit::TestSuite # all tests ?
|
120
|
-
run_without_timing(result, &block)
|
121
|
-
ParallelTests::Test::RuntimeLogger.unique_log
|
122
|
-
else
|
123
|
-
ParallelTests::Test::RuntimeLogger.log_test_run(test.class) do
|
124
|
-
run_without_timing(result, &block)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
95
|
end
|
data/lib/parallel_tests.rb
CHANGED
@@ -88,13 +88,8 @@ module ParallelTests
|
|
88
88
|
pids.count
|
89
89
|
end
|
90
90
|
|
91
|
-
# real time even if someone messed with timecop in tests
|
92
91
|
def now
|
93
|
-
|
94
|
-
Time.now_without_mock_time
|
95
|
-
else
|
96
|
-
Time.now
|
97
|
-
end
|
92
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
98
93
|
end
|
99
94
|
|
100
95
|
def delta
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parallel_tests
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.32.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:
|
11
|
+
date: 2020-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: parallel
|
@@ -62,10 +62,14 @@ files:
|
|
62
62
|
- lib/parallel_tests/test/runner.rb
|
63
63
|
- lib/parallel_tests/test/runtime_logger.rb
|
64
64
|
- lib/parallel_tests/version.rb
|
65
|
-
homepage:
|
65
|
+
homepage: https://github.com/grosser/parallel_tests
|
66
66
|
licenses:
|
67
67
|
- MIT
|
68
|
-
metadata:
|
68
|
+
metadata:
|
69
|
+
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
|
72
|
+
wiki_uri: https://github.com/grosser/parallel_tests/wiki
|
69
73
|
post_install_message:
|
70
74
|
rdoc_options: []
|
71
75
|
require_paths:
|
@@ -74,15 +78,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
78
|
requirements:
|
75
79
|
- - ">="
|
76
80
|
- !ruby/object:Gem::Version
|
77
|
-
version: 2.
|
81
|
+
version: 2.2.0
|
78
82
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
83
|
requirements:
|
80
84
|
- - ">="
|
81
85
|
- !ruby/object:Gem::Version
|
82
86
|
version: '0'
|
83
87
|
requirements: []
|
84
|
-
|
85
|
-
rubygems_version: 2.6.14
|
88
|
+
rubygems_version: 3.0.3
|
86
89
|
signing_key:
|
87
90
|
specification_version: 4
|
88
91
|
summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel
|