parallel_tests 2.28.0 → 3.7.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.
- checksums.yaml +4 -4
- data/Readme.md +46 -28
- data/bin/parallel_cucumber +2 -1
- data/bin/parallel_rspec +2 -1
- data/bin/parallel_spinach +2 -1
- data/bin/parallel_test +2 -1
- data/lib/parallel_tests.rb +12 -12
- data/lib/parallel_tests/cli.rb +133 -68
- data/lib/parallel_tests/cucumber/failures_logger.rb +1 -1
- data/lib/parallel_tests/cucumber/features_with_steps.rb +32 -0
- data/lib/parallel_tests/cucumber/runner.rb +8 -5
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +18 -16
- data/lib/parallel_tests/cucumber/scenarios.rb +20 -30
- data/lib/parallel_tests/gherkin/io.rb +2 -3
- data/lib/parallel_tests/gherkin/listener.rb +10 -12
- data/lib/parallel_tests/gherkin/runner.rb +20 -21
- data/lib/parallel_tests/gherkin/runtime_logger.rb +3 -2
- data/lib/parallel_tests/grouper.rb +92 -28
- data/lib/parallel_tests/pids.rb +4 -3
- data/lib/parallel_tests/railtie.rb +1 -0
- data/lib/parallel_tests/rspec/failures_logger.rb +2 -2
- data/lib/parallel_tests/rspec/logger_base.rb +9 -7
- data/lib/parallel_tests/rspec/runner.rb +27 -12
- data/lib/parallel_tests/rspec/runtime_logger.rb +12 -10
- data/lib/parallel_tests/rspec/summary_logger.rb +2 -3
- data/lib/parallel_tests/spinach/runner.rb +6 -2
- data/lib/parallel_tests/tasks.rb +81 -40
- data/lib/parallel_tests/test/runner.rb +54 -37
- data/lib/parallel_tests/test/runtime_logger.rb +19 -14
- data/lib/parallel_tests/version.rb +2 -1
- metadata +11 -7
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
begin
|
3
|
+
gem "cuke_modeler", "~> 3.0"
|
4
|
+
require 'cuke_modeler'
|
5
|
+
rescue LoadError
|
6
|
+
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.'
|
7
|
+
end
|
8
|
+
|
9
|
+
module ParallelTests
|
10
|
+
module Cucumber
|
11
|
+
class FeaturesWithSteps
|
12
|
+
class << self
|
13
|
+
def all(tests, options)
|
14
|
+
ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
|
15
|
+
# format of hash will be FILENAME => NUM_STEPS
|
16
|
+
steps_per_file = tests.each_with_object({}) do |file, steps|
|
17
|
+
feature = ::CukeModeler::FeatureFile.new(file).feature
|
18
|
+
|
19
|
+
# skip feature if it matches tag regex
|
20
|
+
next if feature.tags.grep(ignore_tag_pattern).any?
|
21
|
+
|
22
|
+
# count the number of steps in the file
|
23
|
+
# will only include a feature if the regex does not match
|
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
|
26
|
+
end
|
27
|
+
steps_per_file.sort_by { |_, value| -value }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,16 +1,21 @@
|
|
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 =
|
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
|
11
12
|
'cucumber'
|
12
13
|
end
|
13
14
|
|
15
|
+
def default_test_folder
|
16
|
+
'features'
|
17
|
+
end
|
18
|
+
|
14
19
|
def line_is_result?(line)
|
15
20
|
super || line =~ SCENARIO_REGEX || line =~ SCENARIOS_RESULTS_BOUNDARY_REGEX
|
16
21
|
end
|
@@ -21,9 +26,7 @@ module ParallelTests
|
|
21
26
|
scenario_groups = results.slice_before(SCENARIOS_RESULTS_BOUNDARY_REGEX).group_by(&:first)
|
22
27
|
scenario_groups.each do |header, group|
|
23
28
|
scenarios = group.flatten.grep(SCENARIO_REGEX)
|
24
|
-
if scenarios.any?
|
25
|
-
output << ([header] + scenarios).join("\n")
|
26
|
-
end
|
29
|
+
output << ([header] + scenarios).join("\n") if scenarios.any?
|
27
30
|
end
|
28
31
|
|
29
32
|
output << super
|
@@ -1,37 +1,34 @@
|
|
1
|
-
|
2
|
-
require 'cucumber/core/gherkin/tag_expression'
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
4
2
|
module ParallelTests
|
5
3
|
module Cucumber
|
6
4
|
module Formatters
|
7
5
|
class ScenarioLineLogger
|
8
6
|
attr_reader :scenarios
|
9
7
|
|
10
|
-
def initialize(tag_expression =
|
8
|
+
def initialize(tag_expression = nil)
|
11
9
|
@scenarios = []
|
12
10
|
@tag_expression = tag_expression
|
13
11
|
end
|
14
12
|
|
15
13
|
def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
|
16
|
-
scenario_tags = feature_element
|
14
|
+
scenario_tags = feature_element.tags.map(&:name)
|
17
15
|
scenario_tags = feature_tags + scenario_tags
|
18
|
-
if feature_element
|
19
|
-
test_line = feature_element
|
16
|
+
if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
|
17
|
+
test_line = feature_element.source_line
|
20
18
|
|
21
19
|
# We don't accept the feature_element if the current tags are not valid
|
22
|
-
return unless
|
20
|
+
return unless matches_tags?(scenario_tags)
|
23
21
|
# or if it is not at the correct location
|
24
22
|
return if line_numbers.any? && !line_numbers.include?(test_line)
|
25
23
|
|
26
|
-
@scenarios << [uri, feature_element
|
24
|
+
@scenarios << [uri, feature_element.source_line].join(":")
|
27
25
|
else # :ScenarioOutline
|
28
|
-
feature_element
|
29
|
-
example_tags = example
|
26
|
+
feature_element.examples.each do |example|
|
27
|
+
example_tags = example.tags.map(&:name)
|
30
28
|
example_tags = scenario_tags + example_tags
|
31
|
-
next unless
|
32
|
-
rows
|
33
|
-
|
34
|
-
test_line = row[:location][:line]
|
29
|
+
next unless matches_tags?(example_tags)
|
30
|
+
example.rows[1..-1].each do |row|
|
31
|
+
test_line = row.source_line
|
35
32
|
next if line_numbers.any? && !line_numbers.include?(test_line)
|
36
33
|
|
37
34
|
@scenarios << [uri, test_line].join(':')
|
@@ -40,7 +37,12 @@ module ParallelTests
|
|
40
37
|
end
|
41
38
|
end
|
42
39
|
|
43
|
-
def method_missing(*
|
40
|
+
def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def matches_tags?(tags)
|
45
|
+
@tag_expression.nil? || @tag_expression.evaluate(tags)
|
44
46
|
end
|
45
47
|
end
|
46
48
|
end
|
@@ -1,21 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'cucumber/tag_expressions/parser'
|
2
|
-
require 'cucumber/core/gherkin/tag_expression'
|
3
3
|
require 'cucumber/runtime'
|
4
4
|
require 'cucumber'
|
5
5
|
require 'parallel_tests/cucumber/scenario_line_logger'
|
6
6
|
require 'parallel_tests/gherkin/listener'
|
7
|
-
require 'gherkin/errors'
|
8
7
|
require 'shellwords'
|
9
8
|
|
9
|
+
begin
|
10
|
+
gem "cuke_modeler", "~> 3.0"
|
11
|
+
require 'cuke_modeler'
|
12
|
+
rescue LoadError
|
13
|
+
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.'
|
14
|
+
end
|
15
|
+
|
10
16
|
module ParallelTests
|
11
17
|
module Cucumber
|
12
18
|
class Scenarios
|
13
19
|
class << self
|
14
|
-
def all(files, options={})
|
20
|
+
def all(files, options = {})
|
15
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
|
16
22
|
tags = []
|
17
23
|
words = options[:test_options].to_s.shellsplit
|
18
|
-
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) }
|
19
25
|
if ignore = options[:ignore_tag_pattern]
|
20
26
|
tags << "not (#{ignore})"
|
21
27
|
end
|
@@ -26,8 +32,7 @@ module ParallelTests
|
|
26
32
|
|
27
33
|
private
|
28
34
|
|
29
|
-
def split_into_scenarios(files, tags='')
|
30
|
-
|
35
|
+
def split_into_scenarios(files, tags = '')
|
31
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
|
32
37
|
# Create the ScenarioLineLogger which will filter the scenario we want
|
33
38
|
args = []
|
@@ -35,37 +40,22 @@ module ParallelTests
|
|
35
40
|
scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(*args)
|
36
41
|
|
37
42
|
# here we loop on the files map, each file will contain one or more scenario
|
38
|
-
|
43
|
+
files.each do |path|
|
39
44
|
# Gather up any line numbers attached to the file path
|
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 = ::
|
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
|
-
|
63
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
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,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
|
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,5 +1,4 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
3
2
|
module ParallelTests
|
4
3
|
module Gherkin
|
5
4
|
class Listener
|
@@ -8,8 +7,10 @@ module ParallelTests
|
|
8
7
|
attr_writer :ignore_tag_pattern
|
9
8
|
|
10
9
|
def initialize
|
11
|
-
@steps
|
10
|
+
@steps = []
|
11
|
+
@uris = []
|
12
12
|
@collect = {}
|
13
|
+
@feature, @ignore_tag_pattern = nil
|
13
14
|
reset_counters!
|
14
15
|
end
|
15
16
|
|
@@ -17,7 +18,7 @@ module ParallelTests
|
|
17
18
|
@feature = feature
|
18
19
|
end
|
19
20
|
|
20
|
-
def background(*
|
21
|
+
def background(*)
|
21
22
|
@background = 1
|
22
23
|
end
|
23
24
|
|
@@ -32,7 +33,7 @@ module ParallelTests
|
|
32
33
|
@outline = 1
|
33
34
|
end
|
34
35
|
|
35
|
-
def step(*
|
36
|
+
def step(*)
|
36
37
|
return if @ignoring
|
37
38
|
if @background == 1
|
38
39
|
@background_steps += 1
|
@@ -52,12 +53,10 @@ module ParallelTests
|
|
52
53
|
# @param [Gherkin::Formatter::Model::Examples] examples
|
53
54
|
#
|
54
55
|
def examples(examples)
|
55
|
-
|
56
|
-
@collect[@uri] += (@outline_steps * examples.rows.size)
|
57
|
-
end
|
56
|
+
@collect[@uri] += (@outline_steps * examples.rows.size) unless examples.rows.empty?
|
58
57
|
end
|
59
58
|
|
60
|
-
def eof(*
|
59
|
+
def eof(*)
|
61
60
|
@collect[@uri] += (@background_steps * @scenarios)
|
62
61
|
reset_counters!
|
63
62
|
end
|
@@ -68,8 +67,7 @@ module ParallelTests
|
|
68
67
|
end
|
69
68
|
|
70
69
|
# ignore lots of other possible callbacks ...
|
71
|
-
def method_missing(*
|
72
|
-
end
|
70
|
+
def method_missing(*); end # rubocop:disable Style/MissingRespondToMissing
|
73
71
|
|
74
72
|
private
|
75
73
|
|
@@ -80,7 +78,7 @@ module ParallelTests
|
|
80
78
|
|
81
79
|
# Set @ignoring if we should ignore this scenario/outline based on its tags
|
82
80
|
def should_ignore(scenario)
|
83
|
-
@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 }
|
84
82
|
end
|
85
83
|
end
|
86
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
|
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?
|
22
|
+
options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
|
21
23
|
|
22
24
|
cmd = [
|
23
25
|
executable,
|
@@ -32,6 +34,10 @@ module ParallelTests
|
|
32
34
|
@test_file_name || 'feature'
|
33
35
|
end
|
34
36
|
|
37
|
+
def default_test_folder
|
38
|
+
'features'
|
39
|
+
end
|
40
|
+
|
35
41
|
def test_suffix
|
36
42
|
/\.feature$/
|
37
43
|
end
|
@@ -44,16 +50,16 @@ module ParallelTests
|
|
44
50
|
# 1 scenario (1 failed)
|
45
51
|
# 1 step (1 failed)
|
46
52
|
def summarize_results(results)
|
47
|
-
sort_order =
|
53
|
+
sort_order = ['scenario', 'step', 'failed', 'flaky', 'undefined', 'skipped', 'pending', 'passed']
|
48
54
|
|
49
|
-
|
55
|
+
['scenario', 'step'].map do |group|
|
50
56
|
group_results = results.grep(/^\d+ #{group}/)
|
51
57
|
next if group_results.empty?
|
52
58
|
|
53
59
|
sums = sum_up_results(group_results)
|
54
60
|
sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
|
55
61
|
sums.map! do |word, number|
|
56
|
-
plural = "s" if word == group
|
62
|
+
plural = "s" if (word == group) && (number != 1)
|
57
63
|
"#{number} #{word}#{plural}"
|
58
64
|
end
|
59
65
|
"#{sums[0]} (#{sums[1..-1].join(", ")})"
|
@@ -61,7 +67,7 @@ module ParallelTests
|
|
61
67
|
end
|
62
68
|
|
63
69
|
def cucumber_opts(given)
|
64
|
-
if given =~ /--profile/
|
70
|
+
if given =~ (/--profile/) || given =~ (/(^|\s)-p /)
|
65
71
|
given
|
66
72
|
else
|
67
73
|
[given, profile_from_config].compact.join(" ")
|
@@ -71,15 +77,11 @@ module ParallelTests
|
|
71
77
|
def profile_from_config
|
72
78
|
# copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
|
73
79
|
config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
|
74
|
-
if config && File.read(config) =~ /^parallel:/
|
75
|
-
"--profile parallel"
|
76
|
-
end
|
80
|
+
"--profile parallel" if config && File.read(config) =~ /^parallel:/
|
77
81
|
end
|
78
82
|
|
79
|
-
def tests_in_groups(tests, num_groups, options={})
|
80
|
-
if options[:group_by] == :scenarios
|
81
|
-
@test_file_name = "scenario"
|
82
|
-
end
|
83
|
+
def tests_in_groups(tests, num_groups, options = {})
|
84
|
+
@test_file_name = "scenario" if options[:group_by] == :scenarios
|
83
85
|
method = "by_#{options[:group_by]}"
|
84
86
|
if Grouper.respond_to?(method)
|
85
87
|
Grouper.send(method, find_tests(tests, options), num_groups, options)
|
@@ -88,7 +90,6 @@ module ParallelTests
|
|
88
90
|
end
|
89
91
|
end
|
90
92
|
|
91
|
-
|
92
93
|
def runtime_logging
|
93
94
|
"--format ParallelTests::Gherkin::RuntimeLogger --out #{runtime_log}"
|
94
95
|
end
|
@@ -98,18 +99,16 @@ module ParallelTests
|
|
98
99
|
end
|
99
100
|
|
100
101
|
def determine_executable
|
101
|
-
|
102
|
-
when File.exist?("bin/#{name}")
|
102
|
+
if File.exist?("bin/#{name}")
|
103
103
|
ParallelTests.with_ruby_binary("bin/#{name}")
|
104
|
-
|
104
|
+
elsif ParallelTests.bundler_enabled?
|
105
105
|
"bundle exec #{name}"
|
106
|
-
|
106
|
+
elsif File.file?("script/#{name}")
|
107
107
|
ParallelTests.with_ruby_binary("script/#{name}")
|
108
108
|
else
|
109
|
-
|
109
|
+
name.to_s
|
110
110
|
end
|
111
111
|
end
|
112
|
-
|
113
112
|
end
|
114
113
|
end
|
115
114
|
end
|