Skip to content

Commit

Permalink
adds the ability to pass --ensure-tag-for-devices="@tag_name:device_i…
Browse files Browse the repository at this point in the history
…d_1,device_id_2,device_id_n", which ensures that certain scenarios can only run on certain devices (for example, fingerprint enabled devices only, etc.)
  • Loading branch information
Rooth, Matthew authored and Rooth, Matthew committed Jul 15, 2016
1 parent 3ced247 commit 456b496
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 30 deletions.
5 changes: 5 additions & 0 deletions bin/parallel_calabash
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def parse_arguments(arguments)
options[:group_by_scenarios] = true
end

opts.on('--ensure-tag-for-devices="@tag:device_filter"', 'Ensure certain tags only run on certain devices') do |device_tag_filter|
options[:ensure_tag_for_devices] ||= []
options[:ensure_tag_for_devices] << device_tag_filter
end

end

opt_parser.parse!(arguments)
Expand Down
2 changes: 1 addition & 1 deletion lib/parallel_calabash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def run_tests_in_parallel
number_of_processes = number_of_processes_to_start
test_results = nil
report_time_taken do
groups = FeatureGrouper.feature_groups(@options, number_of_processes)
groups = FeatureGrouper.feature_groups(@options, number_of_processes, @helper.connected_devices_with_model_info)
threads = groups.size
puts "Running with #{threads} threads: #{groups}"
complete = []
Expand Down
155 changes: 126 additions & 29 deletions lib/parallel_calabash/feature_grouper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
module ParallelCalabash
class FeatureGrouper

DEVICE_TAG_FILTER_REGEX = /([^:]+):([^,]+)(?:,([^,]+))*/

class << self

def feature_groups(options, group_size)
def feature_groups(options, group_size, device_info=[])
return concurrent_feature_groups(options[:feature_folder], group_size) if options[:concurrent]
return scenario_groups group_size, options if options[:group_by_scenarios]
return feature_groups_by_weight(options[:feature_folder], group_size,options[:distribution_tag]) if options[:distribution_tag]
return ensure_tag_for_device_scenario_groups(group_size, options, device_info) if options[:ensure_tag_for_devices]
return scenario_groups(group_size, options) if options[:group_by_scenarios]
return feature_groups_by_weight(options[:feature_folder], group_size, options[:distribution_tag]) if options[:distribution_tag]
feature_groups_by_feature_files(options[:feature_folder], group_size)
end

def feature_groups_by_scenarios(features_scenarios,group_size)
def feature_groups_by_scenarios(features_scenarios, group_size)
puts "Scenarios: #{features_scenarios.size}"
min_number_scenarios_per_group = features_scenarios.size/group_size
remaining_number_of_scenarios = features_scenarios.size % group_size
Expand All @@ -29,17 +32,17 @@ def feature_groups_by_scenarios(features_scenarios,group_size)

def concurrent_feature_groups(feature_folder, number_of_groups)
groups = []
(0...number_of_groups).each{ groups << feature_files_in_folder(feature_folder) }
(0...number_of_groups).each { groups << feature_files_in_folder(feature_folder) }
groups
end

def feature_groups_by_feature_files(feature_folder, group_size)
files = feature_files_in_folder feature_folder
groups = group_creator group_size,files
groups = group_creator(group_size, files)
groups.reject(&:empty?)
end

def group_creator group_size, files
def group_creator(group_size, files)
min_number_files_per_group = files.size/group_size
remaining_number_of_files = files.size % group_size
groups = Array.new(group_size) { [] }
Expand All @@ -54,43 +57,136 @@ def group_creator group_size, files
groups.reject &:empty?
end

def scenario_groups group_size, options
def generate_distribution_data(options)
generate_dry_run_report options
raise "Can not create dry run for scenario distribution" unless File.exists?("parallel_calabash_dry_run.json")
distribution_data = JSON.parse(File.read("parallel_calabash_dry_run.json"))
# puts "SCENARIO GROUPS #{distribution_data}"
raise 'Can not create dry run for scenario distribution' unless File.exists?('parallel_calabash_dry_run.json')
JSON.parse(File.read('parallel_calabash_dry_run.json'))
end

def ensure_tag_for_device_scenario_groups(group_size, options, device_info)
device_tag_filters = parse_device_tag_filters(options[:ensure_tag_for_devices])
distribution_data = generate_distribution_data(options)
groups = Array.new(group_size) { [] }
device_info.map(&:first).each_with_index do |device_id, device_index|
matching_tags = device_tag_filters.map do |tag, device_ids|
tag unless device_ids.select { |curr_id| device_id.start_with?(curr_id) }.empty?
end.compact
distribute_to_device_group_if_necessary(groups, device_index, distribution_data, matching_tags)
end
distribute_remaining_across_groups(groups, distribution_data)
groups
end

def distribute_to_device_group_if_necessary(groups, device_index, distribution_data, matching_tags)
distribution_data.each do |feature|
feature_uri = feature['uri']
feature_matched = tag_match(feature, matching_tags)
feature['elements'].each do |scenario|
scenario_matched = tag_match(scenario, matching_tags)
if scenario['keyword'] == 'Scenario'
if feature_matched || scenario_matched
add_test_to_device_group(groups[device_index], feature_uri, scenario)
end
elsif scenario['keyword'] == 'Scenario Outline'
if scenario['examples']
scenario['examples'].each do |example|
if tag_match(example, matching_tags) || feature_matched || scenario_matched
add_test_to_device_group(groups[device_index], feature_uri, example)
end
end
else
if feature_matched || scenario_matched
add_test_to_device_group(groups[device_index], feature_uri, scenario)
end
end
end
end
end
end

def distribute_remaining_across_groups(groups, distribution_data)
distribution_data.each do |feature|
feature_uri = feature['uri']
feature['elements'].each do |scenario|
if scenario['keyword'] == 'Scenario'
distribute_unless_already(groups, feature_uri, scenario)
elsif scenario['keyword'] == 'Scenario Outline'
if scenario['examples']
scenario['examples'].each do |example|
distribute_unless_already(groups, feature_uri, example)
end
else
distribute_unless_already(groups, feature_uri, scenario)
end
end
end
end
end

def distribute_unless_already(groups, feature_uri, element)
groups.min_by(&:size) << "#{feature_uri}:#{element['line']}" unless element['distributed']
end

def add_test_to_device_group(device_group, feature_uri, element)
device_group << "#{feature_uri}:#{element['line']}"
element['distributed'] = true
end

def tag_match(element, matching_tags)
(matching_tags - element.fetch('tags', []).map { |tag| tag['name'] }).size < matching_tags.size
end

def parse_device_tag_filters(raw_device_tag_filters)
device_tag_filters = {}
raw_device_tag_filters.each do |raw_device_tag_filter|
unless raw_device_tag_filter =~ DEVICE_TAG_FILTER_REGEX
raise "#{raw_device_tag_filter} not in required format. Must be e.g. @tag_name:device_id_1,device_id_2"
end
captures = raw_device_tag_filter.match(DEVICE_TAG_FILTER_REGEX).captures
tag = captures.shift
device_filters = []
until (device_filter=captures.shift).nil?
device_filters << device_filter
end
device_tag_filters[tag] = device_filters
end
device_tag_filters
end

def scenario_groups(group_size, options)
distribution_data = generate_distribution_data(options)
all_runnable_scenarios = distribution_data.map do |feature|
unless feature["elements"].nil?
feature["elements"].map do |scenario|
if scenario["keyword"] == 'Scenario'
"#{feature["uri"]}:#{scenario["line"]}"
unless feature['elements'].nil?
feature['elements'].map do |scenario|
if scenario['keyword'] == 'Scenario'
"#{feature['uri']}:#{scenario['line']}"
elsif scenario['keyword'] == 'Scenario Outline'
if scenario["examples"]
scenario["examples"].map { |example|
"#{feature["uri"]}:#{example["line"]}"
if scenario['examples']
scenario['examples'].map { |example|
"#{feature['uri']}:#{example['line']}"
}
else
"#{feature["uri"]}:#{scenario["line"]}" # Cope with --expand
"#{feature['uri']}:#{scenario['line']}" # Cope with --expand
end
end
end
end
end.flatten.compact
groups = group_creator group_size,all_runnable_scenarios
group_creator(group_size, all_runnable_scenarios)
end

def generate_dry_run_report options
def generate_dry_run_report(options)
%x( cucumber #{options[:cucumber_options]} --dry-run -f json --out parallel_calabash_dry_run.json #{options[:feature_folder].join(' ')} )
end

def feature_files_in_folder(feature_dir_or_file)
if File.directory?(feature_dir_or_file.first)
files = Dir[File.join(feature_dir_or_file, "**{,/*/**}/*")].uniq
files = Dir[File.join(feature_dir_or_file, '**{,/*/**}/*')].uniq
files.grep(/\.feature$/)
elsif feature_folder_has_single_feature?(feature_dir_or_file)
feature_dir_or_file
elsif File.file?(feature_dir_or_file.first)
scenarios = File.open(feature_dir_or_file.first).collect{ |line| line.split(' ') }
scenarios = File.open(feature_dir_or_file.first).collect { |line| line.split(' ') }
scenarios.flatten
end
end
Expand All @@ -115,24 +211,25 @@ def features_with_weights(feature_dir, weighing_factor)

def feature_groups_by_weight(feature_folder, group_size, weighing_factor)
features = features_with_weights feature_folder, weighing_factor
feature_groups = Array.new(group_size).map{|e| e = []}
feature_groups = Array.new(group_size).map { |e| e = [] }
features.each do |feature|
feature_groups[index_of_lightest_group(feature_groups)] << feature
end
feature_groups.reject!{|group| group.empty?}
feature_groups.map{|group| group.map{|feature_hash| feature_hash[:feature]}}
feature_groups.reject! { |group| group.empty? }
feature_groups.map { |group| group.map { |feature_hash| feature_hash[:feature] } }
end

def index_of_lightest_group feature_groups
def index_of_lightest_group(feature_groups)
lightest = feature_groups.min { |x, y| weight_of_group(x) <=> weight_of_group(y) }
index = feature_groups.index(lightest)
feature_groups.index(lightest)
end

def weight_of_group group
def weight_of_group(group)
group.inject(0) { |sum, b| sum + b[:weight] }
end

end

end

end

0 comments on commit 456b496

Please sign in to comment.