parallel_tests 3.0.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'cucumber/formatter/rerun'
2
3
  require 'parallel_tests/gherkin/io'
3
4
 
@@ -21,7 +22,6 @@ module ParallelTests
21
22
  end
22
23
  end
23
24
  end
24
-
25
25
  end
26
26
  end
27
27
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  begin
2
3
  gem "cuke_modeler", "~> 3.0"
3
4
  require 'cuke_modeler'
@@ -12,7 +13,7 @@ module ParallelTests
12
13
  def all(tests, options)
13
14
  ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
14
15
  # format of hash will be FILENAME => NUM_STEPS
15
- steps_per_file = tests.each_with_object({}) do |file,steps|
16
+ steps_per_file = tests.each_with_object({}) do |file, steps|
16
17
  feature = ::CukeModeler::FeatureFile.new(file).feature
17
18
 
18
19
  # skip feature if it matches tag regex
@@ -20,8 +21,8 @@ module ParallelTests
20
21
 
21
22
  # count the number of steps in the file
22
23
  # 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,:+)
24
+ all_steps = feature.scenarios.map { |a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
25
+ steps[file] = all_steps.sum
25
26
  end
26
27
  steps_per_file.sort_by { |_, value| -value }
27
28
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/gherkin/runner"
2
3
 
3
4
  module ParallelTests
4
5
  module Cucumber
5
6
  class Runner < ParallelTests::Gherkin::Runner
6
- SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/
7
- SCENARIO_REGEX = /^cucumber features\/.+:\d+/
7
+ SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/.freeze
8
+ SCENARIO_REGEX = %r{^cucumber features/.+:\d+}.freeze
8
9
 
9
10
  class << self
10
11
  def name
@@ -21,9 +22,7 @@ module ParallelTests
21
22
  scenario_groups = results.slice_before(SCENARIOS_RESULTS_BOUNDARY_REGEX).group_by(&:first)
22
23
  scenario_groups.each do |header, group|
23
24
  scenarios = group.flatten.grep(SCENARIO_REGEX)
24
- if scenarios.any?
25
- output << ([header] + scenarios).join("\n")
26
- end
25
+ output << ([header] + scenarios).join("\n") if scenarios.any?
27
26
  end
28
27
 
29
28
  output << super
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
3
  module Cucumber
3
4
  module Formatters
@@ -10,7 +11,7 @@ module ParallelTests
10
11
  end
11
12
 
12
13
  def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
13
- scenario_tags = feature_element.tags.map { |tag| tag.name }
14
+ scenario_tags = feature_element.tags.map(&:name)
14
15
  scenario_tags = feature_tags + scenario_tags
15
16
  if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
16
17
  test_line = feature_element.source_line
@@ -36,8 +37,7 @@ module ParallelTests
36
37
  end
37
38
  end
38
39
 
39
- def method_missing(*args)
40
- end
40
+ def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing
41
41
 
42
42
  private
43
43
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'cucumber/tag_expressions/parser'
2
3
  require 'cucumber/runtime'
3
4
  require 'cucumber'
@@ -16,11 +17,11 @@ module ParallelTests
16
17
  module Cucumber
17
18
  class Scenarios
18
19
  class << self
19
- def all(files, options={})
20
+ def all(files, options = {})
20
21
  # 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
21
22
  tags = []
22
23
  words = options[:test_options].to_s.shellsplit
23
- words.each_with_index { |w,i| tags << words[i+1] if ["-t", "--tags"].include?(w) }
24
+ words.each_with_index { |w, i| tags << words[i + 1] if ["-t", "--tags"].include?(w) }
24
25
  if ignore = options[:ignore_tag_pattern]
25
26
  tags << "not (#{ignore})"
26
27
  end
@@ -31,8 +32,7 @@ module ParallelTests
31
32
 
32
33
  private
33
34
 
34
- def split_into_scenarios(files, tags='')
35
-
35
+ def split_into_scenarios(files, tags = '')
36
36
  # 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
37
37
  # Create the ScenarioLineLogger which will filter the scenario we want
38
38
  args = []
@@ -40,7 +40,7 @@ module ParallelTests
40
40
  scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(*args)
41
41
 
42
42
  # here we loop on the files map, each file will contain one or more scenario
43
- features ||= files.map do |path|
43
+ files.each do |path|
44
44
  # Gather up any line numbers attached to the file path
45
45
  path, *test_lines = path.split(/:(?=\d+)/)
46
46
  test_lines.map!(&:to_i)
@@ -1,9 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'parallel_tests'
2
3
 
3
4
  module ParallelTests
4
5
  module Gherkin
5
6
  module Io
6
-
7
7
  def prepare_io(path_or_io)
8
8
  if path_or_io.respond_to?(:write)
9
9
  path_or_io
@@ -24,7 +24,7 @@ module ParallelTests
24
24
 
25
25
  # do not let multiple processes get in each others way
26
26
  def lock_output
27
- if File === @io
27
+ if @io.is_a?(File)
28
28
  begin
29
29
  @io.flock File::LOCK_EX
30
30
  yield
@@ -35,7 +35,6 @@ module ParallelTests
35
35
  yield
36
36
  end
37
37
  end
38
-
39
38
  end
40
39
  end
41
40
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
3
  module Gherkin
3
4
  class Listener
@@ -6,7 +7,8 @@ module ParallelTests
6
7
  attr_writer :ignore_tag_pattern
7
8
 
8
9
  def initialize
9
- @steps, @uris = [], []
10
+ @steps = []
11
+ @uris = []
10
12
  @collect = {}
11
13
  @feature, @ignore_tag_pattern = nil
12
14
  reset_counters!
@@ -16,7 +18,7 @@ module ParallelTests
16
18
  @feature = feature
17
19
  end
18
20
 
19
- def background(*args)
21
+ def background(*)
20
22
  @background = 1
21
23
  end
22
24
 
@@ -31,7 +33,7 @@ module ParallelTests
31
33
  @outline = 1
32
34
  end
33
35
 
34
- def step(*args)
36
+ def step(*)
35
37
  return if @ignoring
36
38
  if @background == 1
37
39
  @background_steps += 1
@@ -51,12 +53,10 @@ module ParallelTests
51
53
  # @param [Gherkin::Formatter::Model::Examples] examples
52
54
  #
53
55
  def examples(examples)
54
- if examples.rows.size > 0
55
- @collect[@uri] += (@outline_steps * examples.rows.size)
56
- end
56
+ @collect[@uri] += (@outline_steps * examples.rows.size) unless examples.rows.empty?
57
57
  end
58
58
 
59
- def eof(*args)
59
+ def eof(*)
60
60
  @collect[@uri] += (@background_steps * @scenarios)
61
61
  reset_counters!
62
62
  end
@@ -67,8 +67,7 @@ module ParallelTests
67
67
  end
68
68
 
69
69
  # ignore lots of other possible callbacks ...
70
- def method_missing(*args)
71
- end
70
+ def method_missing(*); end # rubocop:disable Style/MissingRespondToMissing
72
71
 
73
72
  private
74
73
 
@@ -79,7 +78,7 @@ module ParallelTests
79
78
 
80
79
  # Set @ignoring if we should ignore this scenario/outline based on its tags
81
80
  def should_ignore(scenario)
82
- @ignoring = @ignore_tag_pattern && all_tags(scenario).find{ |tag| @ignore_tag_pattern === tag.name }
81
+ @ignoring = @ignore_tag_pattern && all_tags(scenario).find { |tag| @ignore_tag_pattern === tag.name }
83
82
  end
84
83
  end
85
84
  end
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/test/runner"
2
3
  require 'shellwords'
3
4
 
4
5
  module ParallelTests
5
6
  module Gherkin
6
7
  class Runner < ParallelTests::Test::Runner
7
-
8
8
  class << self
9
9
  def run_tests(test_files, process_number, num_processes, options)
10
10
  combined_scenarios = test_files
11
11
 
12
12
  if options[:group_by] == :scenarios
13
13
  grouped = test_files.map { |t| t.split(':') }.group_by(&:first)
14
- combined_scenarios = grouped.map {|file,files_and_lines| "#{file}:#{files_and_lines.map(&:last).join(':')}" }
14
+ combined_scenarios = grouped.map do |file, files_and_lines|
15
+ "#{file}:#{files_and_lines.map(&:last).join(':')}"
16
+ end
15
17
  end
16
18
 
17
19
  sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
18
20
 
19
21
  options[:env] ||= {}
20
- options[:env] = options[:env].merge({'AUTOTEST' => '1'}) if $stdout.tty? # display color when we are in a terminal
22
+ options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
21
23
 
22
24
  cmd = [
23
25
  executable,
@@ -44,16 +46,16 @@ module ParallelTests
44
46
  # 1 scenario (1 failed)
45
47
  # 1 step (1 failed)
46
48
  def summarize_results(results)
47
- sort_order = %w[scenario step failed flaky undefined skipped pending passed]
49
+ sort_order = ['scenario', 'step', 'failed', 'flaky', 'undefined', 'skipped', 'pending', 'passed']
48
50
 
49
- %w[scenario step].map do |group|
51
+ ['scenario', 'step'].map do |group|
50
52
  group_results = results.grep(/^\d+ #{group}/)
51
53
  next if group_results.empty?
52
54
 
53
55
  sums = sum_up_results(group_results)
54
56
  sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
55
57
  sums.map! do |word, number|
56
- plural = "s" if word == group and number != 1
58
+ plural = "s" if (word == group) && (number != 1)
57
59
  "#{number} #{word}#{plural}"
58
60
  end
59
61
  "#{sums[0]} (#{sums[1..-1].join(", ")})"
@@ -61,7 +63,7 @@ module ParallelTests
61
63
  end
62
64
 
63
65
  def cucumber_opts(given)
64
- if given =~ /--profile/ or given =~ /(^|\s)-p /
66
+ if given =~ (/--profile/) || given =~ (/(^|\s)-p /)
65
67
  given
66
68
  else
67
69
  [given, profile_from_config].compact.join(" ")
@@ -71,15 +73,11 @@ module ParallelTests
71
73
  def profile_from_config
72
74
  # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
73
75
  config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
74
- if config && File.read(config) =~ /^parallel:/
75
- "--profile parallel"
76
- end
76
+ "--profile parallel" if config && File.read(config) =~ /^parallel:/
77
77
  end
78
78
 
79
- def tests_in_groups(tests, num_groups, options={})
80
- if options[:group_by] == :scenarios
81
- @test_file_name = "scenario"
82
- end
79
+ def tests_in_groups(tests, num_groups, options = {})
80
+ @test_file_name = "scenario" if options[:group_by] == :scenarios
83
81
  method = "by_#{options[:group_by]}"
84
82
  if Grouper.respond_to?(method)
85
83
  Grouper.send(method, find_tests(tests, options), num_groups, options)
@@ -88,7 +86,6 @@ module ParallelTests
88
86
  end
89
87
  end
90
88
 
91
-
92
89
  def runtime_logging
93
90
  "--format ParallelTests::Gherkin::RuntimeLogger --out #{runtime_log}"
94
91
  end
@@ -98,18 +95,16 @@ module ParallelTests
98
95
  end
99
96
 
100
97
  def determine_executable
101
- case
102
- when File.exist?("bin/#{name}")
98
+ if File.exist?("bin/#{name}")
103
99
  ParallelTests.with_ruby_binary("bin/#{name}")
104
- when ParallelTests.bundler_enabled?
100
+ elsif ParallelTests.bundler_enabled?
105
101
  "bundle exec #{name}"
106
- when File.file?("script/#{name}")
102
+ elsif File.file?("script/#{name}")
107
103
  ParallelTests.with_ruby_binary("script/#{name}")
108
104
  else
109
- "#{name}"
105
+ name.to_s
110
106
  end
111
107
  end
112
-
113
108
  end
114
109
  end
115
110
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'parallel_tests/gherkin/io'
2
3
 
3
4
  module ParallelTests
@@ -19,7 +20,7 @@ module ParallelTests
19
20
 
20
21
  config.on_event :test_run_finished do |_|
21
22
  lock_output do
22
- @io.puts @example_times.map { |file, time| "#{file}:#{time}" }
23
+ @io.puts(@example_times.map { |file, time| "#{file}:#{time}" })
23
24
  end
24
25
  end
25
26
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ParallelTests
2
3
  class Grouper
3
4
  class << self
@@ -6,30 +7,107 @@ module ParallelTests
6
7
  in_even_groups_by_size(features_with_steps, num_groups)
7
8
  end
8
9
 
9
- def by_scenarios(tests, num_groups, options={})
10
+ def by_scenarios(tests, num_groups, options = {})
10
11
  scenarios = group_by_scenarios(tests, options)
11
12
  in_even_groups_by_size(scenarios, num_groups)
12
13
  end
13
14
 
14
- def in_even_groups_by_size(items, num_groups, options= {})
15
- groups = Array.new(num_groups) { {:items => [], :size => 0} }
15
+ def in_even_groups_by_size(items, num_groups, options = {})
16
+ groups = Array.new(num_groups) { { items: [], size: 0 } }
17
+
18
+ return specify_groups(items, num_groups, options, groups) if options[:specify_groups]
16
19
 
17
20
  # add all files that should run in a single process to one group
18
- (options[:single_process] || []).each do |pattern|
19
- matched, items = items.partition { |item, _size| item =~ pattern }
20
- matched.each { |item, size| add_to_group(groups.first, item, size) }
21
+ single_process_patterns = options[:single_process] || []
22
+
23
+ single_items, items = items.partition do |item, _size|
24
+ single_process_patterns.any? { |pattern| item =~ pattern }
25
+ end
26
+
27
+ isolate_count = isolate_count(options)
28
+
29
+ if isolate_count >= num_groups
30
+ raise 'Number of isolated processes must be less than total the number of processes'
31
+ end
32
+
33
+ if isolate_count >= num_groups
34
+ raise 'Number of isolated processes must be >= total number of processes'
21
35
  end
22
36
 
23
- groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
24
- group_features_by_size(items_to_group(items), groups_to_fill)
37
+ if isolate_count >= 1
38
+ # add all files that should run in a multiple isolated processes to their own groups
39
+ group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
40
+ # group the non-isolated by size
41
+ group_features_by_size(items_to_group(items), groups[isolate_count..-1])
42
+ else
43
+ # add all files that should run in a single non-isolated process to first group
44
+ single_items.each { |item, size| add_to_group(groups.first, item, size) }
45
+
46
+ # group all by size
47
+ group_features_by_size(items_to_group(items), groups)
48
+ end
25
49
 
26
50
  groups.map! { |g| g[:items].sort }
27
51
  end
28
52
 
29
53
  private
30
54
 
55
+ def specify_groups(items, num_groups, options, groups)
56
+ specify_test_process_groups = options[:specify_groups].split('|')
57
+ if specify_test_process_groups.count > num_groups
58
+ raise 'Number of processes separated by pipe must be less than or equal to the total number of processes'
59
+ end
60
+
61
+ all_specified_tests = specify_test_process_groups.map { |group| group.split(',') }.flatten
62
+ specified_items_found, items = items.partition { |item, _size| all_specified_tests.include?(item) }
63
+
64
+ specified_specs_not_found = all_specified_tests - specified_items_found.map(&:first)
65
+ if specified_specs_not_found.any?
66
+ raise "Could not find #{specified_specs_not_found} from --specify-groups in the selected files & folders"
67
+ end
68
+
69
+ if specify_test_process_groups.count == num_groups && items.flatten.any?
70
+ raise(
71
+ <<~ERROR
72
+ The number of groups in --specify-groups matches the number of groups from -n but there were other specs
73
+ found in the selected files & folders not specified in --specify-groups. Make sure -n is larger than the
74
+ number of processes in --specify-groups if there are other specs that need to be run. The specs that aren't run:
75
+ #{items.map(&:first)}
76
+ ERROR
77
+ )
78
+ end
79
+
80
+ # First order the specify_groups into the main groups array
81
+ specify_test_process_groups.each_with_index do |specify_test_process, i|
82
+ groups[i] = specify_test_process.split(',')
83
+ end
84
+
85
+ # Return early when processed specify_groups tests exactly match the items passed in
86
+ return groups if specify_test_process_groups.count == num_groups
87
+
88
+ # Now sort the rest of the items into the main groups array
89
+ specified_range = specify_test_process_groups.count..-1
90
+ remaining_groups = groups[specified_range]
91
+ group_features_by_size(items_to_group(items), remaining_groups)
92
+ # Don't sort all the groups, only sort the ones not specified in specify_groups
93
+ sorted_groups = remaining_groups.map { |g| g[:items].sort }
94
+ groups[specified_range] = sorted_groups
95
+
96
+ groups
97
+ end
98
+
99
+ def isolate_count(options)
100
+ if options[:isolate_count] && options[:isolate_count] > 1
101
+ options[:isolate_count]
102
+ elsif options[:isolate]
103
+ 1
104
+ else
105
+ 0
106
+ end
107
+ end
108
+
31
109
  def largest_first(files)
32
- files.sort_by{|_item, size| size }.reverse
110
+ files.sort_by { |_item, size| size }.reverse
33
111
  end
34
112
 
35
113
  def smallest_group(groups)
@@ -46,7 +124,7 @@ module ParallelTests
46
124
  ParallelTests::Cucumber::FeaturesWithSteps.all(tests, options)
47
125
  end
48
126
 
49
- def group_by_scenarios(tests, options={})
127
+ def group_by_scenarios(tests, options = {})
50
128
  require 'parallel_tests/cucumber/scenarios'
51
129
  ParallelTests::Cucumber::Scenarios.all(tests, options)
52
130
  end