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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 859dbc6f86dedda88f0eddc04d5f07e949e737ff
4
- data.tar.gz: c62eacaa3c1d018957fd94d47d025df1e06e9d6d
2
+ SHA256:
3
+ metadata.gz: f9f48375f06f2320fdda57c5c1340b8aedf1761541d4c802c5a17b6f21cbf858
4
+ data.tar.gz: 8e91a9f26aa2710aa023b6d779d7fdf0e768213262d75b7d9969751a3e825089
5
5
  SHA512:
6
- metadata.gz: b306f589ad75ed0ac4ad636cbdda41f370ab58f036d1013536c997a2dc80fce8662e0ed83e157e7809b514ba2c149b88588af6c252d56531b84daaf775f14fc9
7
- data.tar.gz: ebf66916a6ddb13e185013251f8e1f3a1b5a7be8d84d4815ad79e8adb336e2c6878cd036d4d2e9afa0e9e420cefecf99991f3e894a3b1fc01c14e343e2cd8d8f
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://ci.appveyor.com/api/projects/status/708b1up4pqc34x3y?svg=true)](https://ci.appveyor.com/project/grosser/parallel-tests)
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/parallel_runtime_spec.log`), use the CLI: `parallel_test spec -t rspec --runtime-log my.log`
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
- ### Test::Unit & Minitest 4/5
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 more output
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] to use spring you have to [patch it](https://github.com/grosser/parallel_tests/wiki/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 gotaches to the [Wiki](https://github.com/grosser/parallel_tests/wiki) or even better open a PR :)
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/>
@@ -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
- progress_indicator = simulate_output_for_ci if options[:serialize_stdout]
44
-
45
- Parallel.map(items, :in_threads => num_processes) do |item|
46
- result = yield(item)
47
- if progress_indicator && progress_indicator.alive?
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
- report_time_taken do
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 more output") { options[:verbose] = true }
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
- Thread.new do
325
- interval = ENV.fetch('PARALLEL_TEST_HEARTBEAT_INTERVAL', 60).to_f
326
- loop do
327
- sleep interval
328
- print '.'
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| ::Cucumber::Core::Ast::Tag.new(tag[:location], tag[:name])}
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| ::Cucumber::Core::Ast::Tag.new(tag[:location], tag[:name])}
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
- tags.concat options[:ignore_tag_pattern].to_s.split(/\s*,\s*/).map {|tag| "~#{tag}" }
15
- tags.concat options[:test_options].to_s.scan(/(?:-t|--tags) (~?@[\w,~@]+)/).flatten
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, tags.uniq
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
- scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(tag_expression)
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| ::Cucumber::Core::Ast::Tag.new(tag[:location], tag[:name]) }
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|
@@ -10,6 +10,7 @@ module ParallelTests
10
10
  def initialize
11
11
  @steps, @uris = [], []
12
12
  @collect = {}
13
+ @feature, @ignore_tag_pattern = nil
13
14
  reset_counters!
14
15
  end
15
16
 
@@ -5,22 +5,22 @@ module ParallelTests
5
5
  class RuntimeLogger
6
6
  include Io
7
7
 
8
- def initialize(step_mother, path_or_io, options)
9
- @io = prepare_io(path_or_io)
8
+ def initialize(config)
9
+ @io = prepare_io(config.out_stream)
10
10
  @example_times = Hash.new(0)
11
- end
12
11
 
13
- def before_feature(_)
14
- @start_at = ParallelTests.now.to_f
15
- end
12
+ config.on_event :test_case_started do |_|
13
+ @start_at = ParallelTests.now.to_f
14
+ end
16
15
 
17
- def after_feature(feature)
18
- @example_times[feature.file] += ParallelTests.now.to_f - @start_at
19
- end
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
- def after_features(*args)
22
- lock_output do
23
- @io.puts @example_times.map { |file, time| "#{file}:#{time}" }
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
@@ -2,7 +2,7 @@ require 'json'
2
2
 
3
3
  module ParallelTests
4
4
  class Pids
5
- attr_reader :pids, :file_path, :mutex
5
+ attr_reader :file_path, :mutex
6
6
 
7
7
  def initialize(file_path)
8
8
  @file_path = file_path
@@ -14,17 +14,14 @@ module ParallelTests
14
14
  end
15
15
 
16
16
  def determine_executable
17
- cmd = case
17
+ case
18
18
  when File.exist?("bin/rspec")
19
19
  ParallelTests.with_ruby_binary("bin/rspec")
20
20
  when ParallelTests.bundler_enabled?
21
- cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
22
- "bundle exec #{cmd}"
21
+ "bundle exec rspec"
23
22
  else
24
- %w[spec rspec].detect{|cmd| system "#{cmd} --version > #{DEV_NULL} 2>&1" }
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(/^\.\//, "")
@@ -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:test:purge') ? 'db:test:purge' : 'app:db:test:purge'
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 = "rake db:setup RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
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("rake db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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("rake db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args)
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 == :ruby
102
- # dump then load in parallel
103
- Rake::Task['db:schema:dump'].invoke
104
- Rake::Task['parallel:load_schema'].invoke(args[:count])
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
- # there is no separate dump / load for schema_format :sql -> do it safe and slow
130
+ # slow: dump and load in in serial
107
131
  args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
108
- taskname = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
109
- ParallelTests::Tasks.run_in_parallel("rake #{taskname}", args)
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("rake db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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("rake db:rollback RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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 = "rake #{ParallelTests::Tasks.purge_before_load} db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
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("rake #{ParallelTests::Tasks.purge_before_load} db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args)
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("rake db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
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("RAILS_ENV=#{ParallelTests::Tasks.rails_env} rake #{args.command}", args)
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
- $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
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} --type #{test_framework} " \
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[:verbose]
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
- raise "Too little runtime info" if allowed_missing < 0
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
- # fill gaps with unknown-runtime if given, average otherwise
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
- (tests || []).map do |file_or_folder|
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(options[:suffix]||test_suffix).grep(options[:pattern]||//)
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.flatten.uniq
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
@@ -1,3 +1,3 @@
1
1
  module ParallelTests
2
- VERSION = Version = '2.21.1'
2
+ VERSION = Version = '2.32.0'
3
3
  end
@@ -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
- if Time.respond_to?(:now_without_mock_time) # Timecop
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.21.1
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: 2018-01-26 00:00:00.000000000 Z
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: http://github.com/grosser/parallel_tests
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.0.0
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
- rubyforge_project:
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