parallel_tests 3.0.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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