parallel_tests 2.30.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4583a8f40bb8759f5c8bb3e0f17771a10909385bd93846162e2d636faa18b4f1
4
- data.tar.gz: 59798e0156b9bc5461d4fdae181d02cea5035265af3a7cd45517cbaee74c826e
3
+ metadata.gz: e0830430a73f9d4366617a98f2014e8b737b8f987a284ac959b0e83e8b0d7da1
4
+ data.tar.gz: 358bd27a736baec67ef7775d16d8dd2b935f69f25ca71d7065e3343fea8feee6
5
5
  SHA512:
6
- metadata.gz: c3bceb2a75be6d0e3c65b65a728f8b89f7c1b705c93d7b6e23f7e9cc345bff8dd13d56f9db638c64a55f463d7ca39cf58488e34724e37e906f4713345528ff1f
7
- data.tar.gz: bff90557bf81bb0f1584f6d209c7623aba2ca673c51fc3a10c60ea18655c72826f95bd66326e4368f765d6424daf5ded005bf613088e146ee27735103cd44dc0
6
+ metadata.gz: 18e2aae47f9ceb0d933ac37bde90fda6936febcbd5381b48c70be9f923a347fc79116996f36c532ba9e3efc14bbf249dfd3eb697d89d80b85578bf9ebd9cb2ae
7
+ data.tar.gz: a6768ac48b991d984475921f64e7a7262e530303f150e9ee3e5d5322a8a34ee2a660af8ab626eda535c1be593fbf422049ac423f6bdbe0ee6292c9efc5e1603f
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.
@@ -192,7 +192,6 @@ Setup for non-rails
192
192
 
193
193
  Options are:
194
194
  <!-- copy output from bundle exec ./bin/parallel_test -h -->
195
-
196
195
  -n [PROCESSES] How many processes to use, default: available CPUs
197
196
  -p, --pattern [PATTERN] run tests matching this regex pattern
198
197
  --exclude-pattern [PATTERN] exclude tests matching this regex pattern
@@ -222,12 +221,14 @@ Options are:
222
221
  --ignore-tags [PATTERN] When counting steps ignore scenarios with tags that match this pattern
223
222
  --nice execute test commands with low priority.
224
223
  --runtime-log [PATH] Location of previously recorded test runtimes
225
- --allowed-missing Allowed percentage of missing runtimes (default = 50)
224
+ --allowed-missing [INT] Allowed percentage of missing runtimes (default = 50)
226
225
  --unknown-runtime [FLOAT] Use given number as unknown runtime (otherwise use average time)
226
+ --first-is-1 Use "1" as TEST_ENV_NUMBER to not reuse the default test environment
227
+ --fail-fast Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported
227
228
  --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
229
+ --verbose-process-command Displays only the command that will be executed by each process
230
+ --verbose-rerun-command When there are failures, displays the command executed by each process that failed
231
+ --quiet Print only tests output
231
232
  -v, --version Show Version
232
233
  -h, --help Show this.
233
234
 
@@ -256,7 +257,8 @@ TIPS
256
257
  - Instantly see failures (instead of just a red F) with [rspec-instafail](https://github.com/grosser/rspec-instafail)
257
258
  - Use [rspec-retry](https://github.com/NoRedInk/rspec-retry) (not rspec-rerun) to rerun failed tests.
258
259
  - [JUnit formatter configuration](https://github.com/grosser/parallel_tests/wiki#with-rspec_junit_formatter----by-jgarber)
259
-
260
+ - 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))
261
+
260
262
  ### Cucumber
261
263
 
262
264
  - Add a `parallel: foo` profile to your `config/cucumber.yml` and it will be used to run parallel tests
@@ -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
@@ -41,9 +42,10 @@ module ParallelTests
41
42
  Tempfile.open 'parallel_tests-lock' do |lock|
42
43
  ParallelTests.with_pid_file do
43
44
  simulate_output_for_ci options[:serialize_stdout] do
44
- Parallel.map(items, :in_threads => num_processes) do |item|
45
+ Parallel.map(items, in_threads: num_processes) do |item|
45
46
  result = yield(item)
46
47
  reprint_output(result, lock.path) if options[:serialize_stdout]
48
+ ParallelTests.stop_all_processes if options[:fail_fast] && result[:exit_status] != 0
47
49
  result
48
50
  end
49
51
  end
@@ -219,6 +221,7 @@ module ParallelTests
219
221
  opts.on("--allowed-missing [INT]", Integer, "Allowed percentage of missing runtimes (default = 50)") { |percent| options[:allowed_missing_percent] = percent }
220
222
  opts.on("--unknown-runtime [FLOAT]", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time }
221
223
  opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
224
+ opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true }
222
225
  opts.on("--verbose", "Print debug output") { options[:verbose] = true }
223
226
  opts.on("--verbose-process-command", "Displays only the command that will be executed by each process") { options[:verbose_process_command] = true }
224
227
  opts.on("--verbose-rerun-command", "When there are failures, displays the command executed by each process that failed") { options[:verbose_rerun_command] = true }
@@ -239,7 +242,7 @@ module ParallelTests
239
242
  files, remaining = extract_file_paths(argv)
240
243
  unless options[:execute]
241
244
  abort "Pass files or folders to run" unless files.any?
242
- options[:files] = files
245
+ options[:files] = files.map { |file_path| Pathname.new(file_path).cleanpath.to_s }
243
246
  end
244
247
 
245
248
  append_test_options(options, remaining)
@@ -315,9 +318,8 @@ module ParallelTests
315
318
  end
316
319
 
317
320
  def final_fail_message
318
- fail_message = "#{@runner.name}s Failed"
321
+ fail_message = "Tests Failed"
319
322
  fail_message = "\e[31m#{fail_message}\e[0m" if use_colors?
320
-
321
323
  fail_message
322
324
  end
323
325
 
@@ -0,0 +1,31 @@
1
+ begin
2
+ gem "cuke_modeler", "~> 3.0"
3
+ require 'cuke_modeler'
4
+ rescue LoadError
5
+ raise 'Grouping by number of cucumber steps requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
6
+ end
7
+
8
+ module ParallelTests
9
+ module Cucumber
10
+ class FeaturesWithSteps
11
+ class << self
12
+ def all(tests, options)
13
+ ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
14
+ # format of hash will be FILENAME => NUM_STEPS
15
+ steps_per_file = tests.each_with_object({}) do |file,steps|
16
+ feature = ::CukeModeler::FeatureFile.new(file).feature
17
+
18
+ # skip feature if it matches tag regex
19
+ next if feature.tags.grep(ignore_tag_pattern).any?
20
+
21
+ # count the number of steps in the file
22
+ # will only include a feature if the regex does not match
23
+ all_steps = feature.scenarios.map{|a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
24
+ steps[file] = all_steps.inject(0,:+)
25
+ end
26
+ steps_per_file.sort_by { |_, value| -value }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,37 +1,33 @@
1
- require 'cucumber/tag_expressions/parser'
2
- require 'cucumber/core/gherkin/tag_expression'
3
-
4
1
  module ParallelTests
5
2
  module Cucumber
6
3
  module Formatters
7
4
  class ScenarioLineLogger
8
5
  attr_reader :scenarios
9
6
 
10
- def initialize(tag_expression = ::Cucumber::Core::Gherkin::TagExpression.new([]))
7
+ def initialize(tag_expression = nil)
11
8
  @scenarios = []
12
9
  @tag_expression = tag_expression
13
10
  end
14
11
 
15
12
  def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
16
- scenario_tags = feature_element[:tags].map { |tag| tag[:name] }
13
+ scenario_tags = feature_element.tags.map { |tag| tag.name }
17
14
  scenario_tags = feature_tags + scenario_tags
18
- if feature_element[:examples].nil? # :Scenario
19
- test_line = feature_element[:location][:line]
15
+ if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
16
+ test_line = feature_element.source_line
20
17
 
21
18
  # We don't accept the feature_element if the current tags are not valid
22
- return unless @tag_expression.evaluate(scenario_tags)
19
+ return unless matches_tags?(scenario_tags)
23
20
  # or if it is not at the correct location
24
21
  return if line_numbers.any? && !line_numbers.include?(test_line)
25
22
 
26
- @scenarios << [uri, feature_element[:location][:line]].join(":")
23
+ @scenarios << [uri, feature_element.source_line].join(":")
27
24
  else # :ScenarioOutline
28
- feature_element[:examples].each do |example|
29
- example_tags = example[:tags].map { |tag| tag[:name] }
25
+ feature_element.examples.each do |example|
26
+ example_tags = example.tags.map(&:name)
30
27
  example_tags = scenario_tags + example_tags
31
- next unless @tag_expression.evaluate(example_tags)
32
- rows = example[:tableBody].select { |body| body[:type] == :TableRow }
33
- rows.each do |row|
34
- test_line = row[:location][:line]
28
+ next unless matches_tags?(example_tags)
29
+ example.rows[1..-1].each do |row|
30
+ test_line = row.source_line
35
31
  next if line_numbers.any? && !line_numbers.include?(test_line)
36
32
 
37
33
  @scenarios << [uri, test_line].join(':')
@@ -42,6 +38,12 @@ module ParallelTests
42
38
 
43
39
  def method_missing(*args)
44
40
  end
41
+
42
+ private
43
+
44
+ def matches_tags?(tags)
45
+ @tag_expression.nil? || @tag_expression.evaluate(tags)
46
+ end
45
47
  end
46
48
  end
47
49
  end
@@ -1,12 +1,17 @@
1
1
  require 'cucumber/tag_expressions/parser'
2
- require 'cucumber/core/gherkin/tag_expression'
3
2
  require 'cucumber/runtime'
4
3
  require 'cucumber'
5
4
  require 'parallel_tests/cucumber/scenario_line_logger'
6
5
  require 'parallel_tests/gherkin/listener'
7
- require 'gherkin/errors'
8
6
  require 'shellwords'
9
7
 
8
+ begin
9
+ gem "cuke_modeler", "~> 3.0"
10
+ require 'cuke_modeler'
11
+ rescue LoadError
12
+ raise 'Grouping by individual cucumber scenarios requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
13
+ end
14
+
10
15
  module ParallelTests
11
16
  module Cucumber
12
17
  class Scenarios
@@ -40,32 +45,17 @@ module ParallelTests
40
45
  path, *test_lines = path.split(/:(?=\d+)/)
41
46
  test_lines.map!(&:to_i)
42
47
 
43
- # We encode the file and get the content of it
44
- source = ::Cucumber::Runtime::NormalisedEncodingFile.read(path)
45
48
  # We create a Gherkin document, this will be used to decode the details of each scenario
46
- document = ::Cucumber::Core::Gherkin::Document.new(path, source)
47
-
48
- # We create a parser for the gherkin document
49
- parser = ::Gherkin::Parser.new()
50
- scanner = ::Gherkin::TokenScanner.new(document.body)
51
-
52
- begin
53
- # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
54
- result = parser.parse(scanner)
55
- feature_tags = result[:feature][:tags].map { |tag| tag[:name] }
56
-
57
- # We loop on each children of the feature
58
- result[:feature][:children].each do |feature_element|
59
- # If the type of the child is not a scenario or scenario outline, we continue, we are only interested by the name of the scenario here
60
- next unless /Scenario/.match(feature_element[:type])
49
+ document = ::CukeModeler::FeatureFile.new(path)
50
+ feature = document.feature
61
51
 
62
- # It's a scenario, we add it to the scenario_line_logger
63
- scenario_line_logger.visit_feature_element(document.uri, feature_element, feature_tags, line_numbers: test_lines)
64
- end
52
+ # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
53
+ feature_tags = feature.tags.map(&:name)
65
54
 
66
- rescue StandardError => e
67
- # Exception if the document is no well formated or error in the tags
68
- raise ::Cucumber::Core::Gherkin::ParseError.new("#{document.uri}: #{e.message}")
55
+ # We loop on each children of the feature
56
+ feature.tests.each do |test|
57
+ # It's a scenario, we add it to the scenario_line_logger
58
+ scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
69
59
  end
70
60
  end
71
61
 
@@ -1,5 +1,3 @@
1
- require 'gherkin/parser'
2
-
3
1
  module ParallelTests
4
2
  module Gherkin
5
3
  class Listener
@@ -14,7 +14,7 @@ module ParallelTests
14
14
  end
15
15
 
16
16
  config.on_event :test_case_finished do |event|
17
- @example_times[event.test_case.feature.file] += ParallelTests.now.to_f - @start_at
17
+ @example_times[event.test_case.location.file] += ParallelTests.now.to_f - @start_at
18
18
  end
19
19
 
20
20
  config.on_event :test_run_finished do |_|
@@ -2,7 +2,7 @@ module ParallelTests
2
2
  class Grouper
3
3
  class << self
4
4
  def by_steps(tests, num_groups, options)
5
- features_with_steps = build_features_with_steps(tests, options)
5
+ features_with_steps = group_by_features_with_steps(tests, options)
6
6
  in_even_groups_by_size(features_with_steps, num_groups)
7
7
  end
8
8
 
@@ -41,23 +41,9 @@ module ParallelTests
41
41
  group[:size] += size
42
42
  end
43
43
 
44
- def build_features_with_steps(tests, options)
45
- require 'gherkin/parser'
46
- ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
47
- parser = ::Gherkin::Parser.new
48
- # format of hash will be FILENAME => NUM_STEPS
49
- steps_per_file = tests.each_with_object({}) do |file,steps|
50
- feature = parser.parse(File.read(file)).fetch(:feature)
51
-
52
- # skip feature if it matches tag regex
53
- next if feature[:tags].grep(ignore_tag_pattern).any?
54
-
55
- # count the number of steps in the file
56
- # will only include a feature if the regex does not match
57
- all_steps = feature[:children].map{|a| a[:steps].count if a[:tags].grep(ignore_tag_pattern).empty? }.compact
58
- steps[file] = all_steps.inject(0,:+)
59
- end
60
- steps_per_file.sort_by { |_, value| -value }
44
+ def group_by_features_with_steps(tests, options)
45
+ require 'parallel_tests/cucumber/features_with_steps'
46
+ ParallelTests::Cucumber::FeaturesWithSteps.all(tests, options)
61
47
  end
62
48
 
63
49
  def group_by_scenarios(tests, options={})
@@ -4,8 +4,6 @@ module ParallelTests
4
4
  module RSpec
5
5
  class Runner < ParallelTests::Test::Runner
6
6
  DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
7
- NAME = 'RSpec'
8
-
9
7
  class << self
10
8
  def run_tests(test_files, process_number, num_processes, options)
11
9
  exe = executable # expensive, so we cache
@@ -14,17 +12,14 @@ module ParallelTests
14
12
  end
15
13
 
16
14
  def determine_executable
17
- cmd = case
15
+ case
18
16
  when File.exist?("bin/rspec")
19
17
  ParallelTests.with_ruby_binary("bin/rspec")
20
18
  when ParallelTests.bundler_enabled?
21
- cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
22
- "bundle exec #{cmd}"
19
+ "bundle exec rspec"
23
20
  else
24
- %w[spec rspec].detect{|cmd| system "#{cmd} --version > #{DEV_NULL} 2>&1" }
21
+ "rspec"
25
22
  end
26
-
27
- cmd or raise("Can't find executables rspec or spec")
28
23
  end
29
24
 
30
25
  def runtime_log
@@ -3,15 +3,9 @@ require 'parallel_tests'
3
3
  module ParallelTests
4
4
  module Test
5
5
  class Runner
6
- NAME = 'Test'
7
-
8
6
  class << self
9
7
  # --- usually overwritten by other runners
10
8
 
11
- def name
12
- NAME
13
- end
14
-
15
9
  def runtime_log
16
10
  'tmp/parallel_runtime_test.log'
17
11
  end
@@ -188,11 +182,7 @@ module ParallelTests
188
182
  puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
189
183
  end
190
184
 
191
- # fill gaps with unknown-runtime if given, average otherwise
192
- known, unknown = tests.partition(&:last)
193
- average = (known.any? ? known.map!(&:last).inject(:+) / known.size : 1)
194
- unknown_runtime = options[:unknown_runtime] || average
195
- unknown.each { |set| set[1] = unknown_runtime }
185
+ set_unknown_runtime tests, options
196
186
  end
197
187
 
198
188
  def runtimes(tests, options)
@@ -240,6 +230,16 @@ module ParallelTests
240
230
 
241
231
  private
242
232
 
233
+ # fill gaps with unknown-runtime if given, average otherwise
234
+ # NOTE: an optimization could be doing runtime by average runtime per file size, but would need file checks
235
+ def set_unknown_runtime(tests, options)
236
+ known, unknown = tests.partition(&:last)
237
+ return if unknown.empty?
238
+ unknown_runtime = options[:unknown_runtime] ||
239
+ (known.empty? ? 1 : known.map!(&:last).inject(:+) / known.size) # average
240
+ unknown.each { |set| set[1] = unknown_runtime }
241
+ end
242
+
243
243
  def report_process_command?(options)
244
244
  options[:verbose] || options[:verbose_process_command]
245
245
  end
@@ -1,3 +1,3 @@
1
1
  module ParallelTests
2
- VERSION = Version = '2.30.0'
2
+ VERSION = Version = '3.1.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.30.0
4
+ version: 3.1.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: 2019-12-10 00:00:00.000000000 Z
11
+ date: 2020-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -42,6 +42,7 @@ files:
42
42
  - lib/parallel_tests.rb
43
43
  - lib/parallel_tests/cli.rb
44
44
  - lib/parallel_tests/cucumber/failures_logger.rb
45
+ - lib/parallel_tests/cucumber/features_with_steps.rb
45
46
  - lib/parallel_tests/cucumber/runner.rb
46
47
  - lib/parallel_tests/cucumber/scenario_line_logger.rb
47
48
  - lib/parallel_tests/cucumber/scenarios.rb
@@ -62,10 +63,14 @@ files:
62
63
  - lib/parallel_tests/test/runner.rb
63
64
  - lib/parallel_tests/test/runtime_logger.rb
64
65
  - lib/parallel_tests/version.rb
65
- homepage: http://github.com/grosser/parallel_tests
66
+ homepage: https://github.com/grosser/parallel_tests
66
67
  licenses:
67
68
  - MIT
68
- metadata: {}
69
+ metadata:
70
+ bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v3.1.0/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v3.1.0
73
+ wiki_uri: https://github.com/grosser/parallel_tests/wiki
69
74
  post_install_message:
70
75
  rdoc_options: []
71
76
  require_paths:
@@ -74,14 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
79
  requirements:
75
80
  - - ">="
76
81
  - !ruby/object:Gem::Version
77
- version: 2.2.0
82
+ version: 2.4.0
78
83
  required_rubygems_version: !ruby/object:Gem::Requirement
79
84
  requirements:
80
85
  - - ">="
81
86
  - !ruby/object:Gem::Version
82
87
  version: '0'
83
88
  requirements: []
84
- rubygems_version: 3.0.3
89
+ rubygems_version: 3.1.3
85
90
  signing_key:
86
91
  specification_version: 4
87
92
  summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel