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

Feature/option to allow specific tags to run on specific devices #25

Open
wants to merge 6 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.idea
*.iml
/.bundle/
/.yardoc
/Gemfile.lock
Expand Down
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ gemspec
gem 'rake'
gem 'calabash-android'
gem 'pry'
gem 'rspec'
gem 'rspec'
gem 'minitest', '~> 4.7.5'
51 changes: 27 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,38 @@ Usage: parallel_calabash [options]

Example: parallel_calabash -a my.apk -o 'cucumber_opts_like_tags_profile_etc_here' features/

-h, --help Show this message
-v, --version Show version
-a, --apk apk_path apk file path
-o, --cucumber_opts '[OPTIONS]' execute with those cucumber options
-f, --filter Filter devices to run tests against using partial device id or model name matching. Multiple filters seperated by ','
--serialize-stdout Serialize stdout output, nothing will be written until everything is done
--group-by-scenarios Distribute equally as per scenarios. This uses cucumber dry run
--concurrent Run tests concurrently. Each test will run once on each device.
-h, --help Show this message
-v, --version Show version
-a, --apk apk_path apk file path
-o, --cucumber_opts '[OPTIONS]' execute with those cucumber options
-f, --filter Filter devices to run tests against using partial device id or model name matching. Multiple filters seperated by ','
--serialize-stdout Serialize stdout output, nothing will be written until everything is done
--group-by-scenarios Distribute equally as per scenarios. This uses cucumber dry run
--concurrent Run tests concurrently. Each test will run once on each device.
--ensure-tag-for-devices filter Ensures specific tags are run on specified devices. Filter format example: "@device_a_or_b_specific:deviceAId,deviceBId". Flag be passed multiple times.

## Usage iOS

Example: parallel_calabash -app my.app --ios_config ~/.parallel_calabash.iphoneos -o '-cucumber -opts' -r '-cucumber -reports>' features/

-h, --help Show this message
-v, --version Show version
--app app_path app file path
--device_target target ios target if no .parallel-calabash config
--device_endpoint endpoint ios endpoint if no .parallel-calabash config
--simulator type for simctl create, e.g. 'com.apple.CoreSimulator.SimDeviceType.iPhone-6 com.apple.CoreSimulator.SimRuntime.iOS-8-4'
--ios_config file for ios, configuration for devices and users
-d, --distribution-tag tag divide features into groups as per occurrence of given tag
-f, --filter filter Filter devices to run tests against keys or values in config. Multiple filters seperated by ','
--skip_ios_ping_check Skip the connectivity test for iOS devices
-o, --cucumber_opts '[OPTIONS]' execute with those cucumber options
-r '[REPORTS]', generate these cucumber reports (not during filtering)
--cucumber_reports
--serialize-stdout Serialize stdout output, nothing will be written until everything is done
--concurrent Run tests concurrently. Each test will run once on each device
--group-by-scenarios Distribute equally as per scenarios. This uses cucumber dry run
-h, --help Show this message
-v, --version Show version
--app app_path app file path
--device_target target ios target if no .parallel-calabash config
--device_endpoint endpoint ios endpoint if no .parallel-calabash config
--simulator type for simctl create, e.g. 'com.apple.CoreSimulator.SimDeviceType.iPhone-6 com.apple.CoreSimulator.SimRuntime.iOS-8-4'
--ios_config file for ios, configuration for devices and users
-d, --distribution-tag tag divide features into groups as per occurrence of given tag
-f, --filter filter Filter devices to run tests against keys or values in config. Multiple filters seperated by ','
--skip_ios_ping_check Skip the connectivity test for iOS devices
-o, --cucumber_opts '[OPTIONS]' execute with those cucumber options
-r '[REPORTS]', generate these cucumber reports (not during filtering)
--cucumber_reports
--serialize-stdout Serialize stdout output, nothing will be written until everything is done
--concurrent Run tests concurrently. Each test will run once on each device
--group-by-scenarios Distribute equally as per scenarios. This uses cucumber dry run
--ensure-tag-for-devices filter Ensures specific tags are run on specified devices. Filter format example: "@device_a_or_b_specific:deviceAId,deviceBId". Flag be passed multiple times.


### iOS set-up

Expand Down
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[:features_device_specific] ||= []
options[:features_device_specific] << 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
161 changes: 132 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[:features_device_specific]
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,142 @@ 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[:features_device_specific])
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
ensure_for_device_index_if_necessary(device_index, distribution_data, matching_tags)
end
distribute_across_groups_for_devices(groups, distribution_data)
groups
end

def ensure_for_device_index_if_necessary(device_index, distribution_data, matching_tags)
distribution_data.each do |feature|
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
ensure_for_device_index(device_index, 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
ensure_for_device_index(device_index, example)
end
end
else
if feature_matched || scenario_matched
ensure_for_device_index(device_index, scenario)
end
end
end
end
end
end

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

def distribute_for_devices(groups, feature_uri, element, device_specific)
if element['device-specific-indexes'] && device_specific
group = element['device-specific-indexes'].map { |device_index| groups[device_index] }.min_by(&:size)
group << "#{feature_uri}:#{element['line']}"
elsif !(element['device-specific-indexes'] || device_specific)
groups.min_by(&:size) << "#{feature_uri}:#{element['line']}"
end
end

def ensure_for_device_index(device_index, element)
element['device-specific-indexes'] ||= []
element['device-specific-indexes'] << device_index
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 +217,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
1 change: 0 additions & 1 deletion spec/lib/parallel_calabash/adb_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require 'spec_helper'
require 'minitest/mock'
require 'parallel_calabash/adb_helper'
describe ParallelCalabash::AdbHelper do

Expand Down
Loading