Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --single-process-tag option to run specific Gherkin scenarios sequentially #810

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/parallel_tests/cli.rb
Expand Up @@ -208,7 +208,11 @@ def parse_options!(argv)
(options[:single_process] ||= []) << /#{pattern}/
end

opts.on("-i", "--isolate", "Do not run any other tests in the group used by --single(-s)") do
opts.on("--single-tag [TAG]", "Run all Cucumber/Gherkin scenarios with specified tag in the same process") do |tag|
options[:single_process_tag] = tag
end

opts.on("-i", "--isolate", "Do not run any other tests in the group used by --single(-s) or --single-tag") do
options[:isolate] = true
end

Expand Down
4 changes: 2 additions & 2 deletions lib/parallel_tests/cucumber/scenario_line_logger.rb
Expand Up @@ -21,7 +21,7 @@ def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
# or if it is not at the correct location
return if line_numbers.any? && !line_numbers.include?(test_line)

@scenarios << [uri, feature_element.source_line].join(":")
@scenarios << [[uri, feature_element.source_line].join(":"), scenario_tags]
else # :ScenarioOutline
feature_element.examples.each do |example|
example_tags = example.tags.map(&:name)
Expand All @@ -31,7 +31,7 @@ def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
test_line = row.source_line
next if line_numbers.any? && !line_numbers.include?(test_line)

@scenarios << [uri, test_line].join(':')
@scenarios << [[uri, test_line].join(':'), scenario_tags]
end
end
end
Expand Down
56 changes: 47 additions & 9 deletions lib/parallel_tests/grouper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module ParallelTests
class Grouper
BY_SCENARIOS_SUPPORTED_OPTIONS = [:single_process_tag].freeze

class << self
def by_steps(tests, num_groups, options)
features_with_steps = group_by_features_with_steps(tests, options)
Expand All @@ -9,20 +11,16 @@ def by_steps(tests, num_groups, options)

def by_scenarios(tests, num_groups, options = {})
scenarios = group_by_scenarios(tests, options)
in_even_groups_by_size(scenarios, num_groups)
in_even_groups_by_size(scenarios, num_groups, options.slice(*BY_SCENARIOS_SUPPORTED_OPTIONS))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could just pass in all options and let it pick what it wants

end

def in_even_groups_by_size(items, num_groups, options = {})
groups = Array.new(num_groups) { { items: [], size: 0 } }

return specify_groups(items, num_groups, options, groups) if options[:specify_groups]

# add all files that should run in a single process to one group
single_process_patterns = options[:single_process] || []

single_items, items = items.partition do |item, _size|
single_process_patterns.any? { |pattern| item =~ pattern }
end
# add all files/scenarios that should run in a single process to one group
single_items, items = separate_single_items(items, options)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice refactor!


isolate_count = isolate_count(options)

Expand All @@ -41,7 +39,7 @@ def in_even_groups_by_size(items, num_groups, options = {})
group_features_by_size(items_to_group(items), groups[isolate_count..-1])
else
# add all files that should run in a single non-isolated process to first group
single_items.each { |item, size| add_to_group(groups.first, item, size) }
group_features_by_size(items_to_group(single_items), [groups.first])

# group all by size
group_features_by_size(items_to_group(items), groups)
Expand Down Expand Up @@ -129,6 +127,23 @@ def group_by_scenarios(tests, options = {})
ParallelTests::Cucumber::Scenarios.all(tests, options)
end

def separate_single_items(items, options)
items.partition { |item| to_single_items?(item, options) }
end

def to_single_items?(item, options)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def to_single_items?(item, options)
def single_item?(item, options)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole "item is item or item and size or item and tags" is kinda funky ... if you have any good ideas how to clean that up lmk or leave a TODO comment on how that can be improved

if options[:single_process]
item = item_with_tags?(item) || item_with_size?(item) ? item[0] : item
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
item = item_with_tags?(item) || item_with_size?(item) ? item[0] : item
item = item[0] if item_with_tags?(item) || item_with_size?(item)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... maybe just if item.is_a?(Array) ... but not sure

options[:single_process].any? { |pattern| item =~ pattern }
elsif options[:single_process_tag]
raise "--single-tag option can only be used with '--group-by scenarios'" unless item_with_tags?(item)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put the raise into the option parser so it fails early ?

item_tags = item[1]
item_tags.any? { |tag| tag.match?(options[:single_process_tag]) }
else
false
end
end

def group_features_by_size(items, groups_to_fill)
items.each do |item, size|
size ||= 1
Expand All @@ -138,7 +153,30 @@ def group_features_by_size(items, groups_to_fill)
end

def items_to_group(items)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some rough comment on what this method is used/does for would be nice

items.first && items.first.size == 2 ? largest_first(items) : items
return items_without_tags(items) if items_with_tags?(items)
return largest_first(items) if items_with_size?(items)

items
end

def items_with_tags?(items)
items.first.is_a?(Array) && item_with_tags?(items.first)
end

def items_with_size?(items)
items.first.is_a?(Array) && item_with_size?(items.first)
end

def item_with_tags?(item)
item[1].is_a?(Array)
end

def item_with_size?(item)
item[1].is_a?(Numeric)
end

def items_without_tags(items)
items.map(&:first)
end
end
end
Expand Down