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

Adding support for basic multiple simulator runner alongside different user based approach #17

Open
wants to merge 16 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
Expand Up @@ -13,4 +13,5 @@
*.o
*.a
mkmf.log
.DS_Store
test_files
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 'run_loop'
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Example: parallel_calabash -app my.app --ios_config ~/.parallel_calabash.iphoneo
-v, --version Show version
--app app_path app file path
--device_target target ios target if no .parallel-calabash config
--num_emulators number number of parallel ios emulators to launch for same user (SSH running will be avoided)
--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
Expand All @@ -67,7 +68,20 @@ Example: parallel_calabash -app my.app --ios_config ~/.parallel_calabash.iphoneo
### iOS set-up

* iOS testing is only supported on MacOS hosts.
* Create as many (Administrator-privileged!) test accounts as you have devices or want simulators (Settings > Users & Groups)
* For basic run, use the below command and launch the parallel tests
```
$ parallel_calabash -app my.app --num_emulators 3 --simulator 'com.apple.CoreSimulator.SimDeviceType.iPhone-6 com.apple.CoreSimulator.SimRuntime.iOS-8-4' -o '-cucumber -opts' -r '-cucumber -reports>' features/
```
* You would require to add following code to your env.rb file

```ruby
if ENV["TEST_PROCESS_NUMBER"]
require 'parallel_calabash/ios/patches'
launcher.relaunch(timeout: 90, relaunch_simulator: false, quit_sim_on_init: false)
end
```

* For different user based control, create as many (Administrator-privileged!) test accounts as you have devices or want simulators (Settings > Users & Groups)
* As the main user, the one that runs parallel_calabash, create ~/.parallel_calabash.iphonesimulator and/or ~/.parallel_calabash.iphoneos

As follows:
Expand Down
4 changes: 4 additions & 0 deletions bin/parallel_calabash
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def parse_arguments(arguments)
options[:device_target] = device_target
end

opts.on('--num_emulators number', 'number of parallel ios emulators to launch for same user (SSH running will be avoided)') do |num_emulators|
options[:num_emulators] = num_emulators
end

opts.on('--device_endpoint endpoint', 'ios endpoint if no .parallel-calabash config') do |device_endpoint|
options[:device_endpoint] = device_endpoint
end
Expand Down
1 change: 1 addition & 0 deletions lib/parallel_calabash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def initialize(options)
{
DEVICE_TARGET: options[:device_target],
DEVICE_ENDPOINT: options[:device_endpoint],
num_emulators: options[:num_emulators]
},
options[:ios_config]
)
Expand Down
15 changes: 15 additions & 0 deletions lib/parallel_calabash/adb_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def initialize(filter = nil, default_simulator = nil, config_file = nil, instrum
else
@config = File.exist?(config_file) ? eval(File.read(config_file)) : {}
end
@no_of_devices = (@default_simulator[:num_emulators] || @config[:NO_OF_DEVICES]).to_i
@instruments = instruments || %x(instruments -s devices ; echo) # Bizarre workaround for xcode 7
end

Expand All @@ -78,13 +79,27 @@ def connected_devices_with_model_info
if @config[:DEVICES]
configs = apply_filter(compute_devices)
fail '** No devices (or users) unfiltered!' if configs.empty?
elsif @no_of_devices > 0
configs = compute_simulators_to_create
else
configs = apply_filter(compute_simulators)
configs = configs.empty? ? [@default_simulator] : configs
end
@devices = configs
end

def compute_simulators_to_create
port = (@config[:CALABASH_SERVER_PORT] || 28000).to_i
simulator = @config[:DEVICE_TARGET] || nil
(1..@no_of_devices).map do |i|
{}.tap do |my_hash|
my_hash[:SIMULATOR] = simulator unless simulator.nil?
my_hash[:DEVICE_NAME] = "PCalSimulator_#{i}"
my_hash[:CALABASH_SERVER_PORT] = port + i - 1
end
end
end

def compute_simulators
port = (@config[:CALABASH_SERVER_PORT] || 28000).to_i
users = @config[:USERS] || []
Expand Down
31 changes: 31 additions & 0 deletions lib/parallel_calabash/ios/connectivity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module ParallelCalabash
class TimeoutErr < RuntimeError
end
module Ios
class Connectivity
def self.ensure launcher
raise "ping_app not present in launcher!" unless launcher.respond_to? "ping_app"
connected = false
until connected do
begin
Timeout::timeout(90, TimeoutErr) do
until connected
begin
connected = (launcher.ping_app == '200')
break if connected
rescue Exception => e
puts "Retry connection after 1 second"
ensure
sleep 1 unless connected
end
end
end
rescue TimeoutErr => e
puts 'Timed out... exiting'
break
end
end
end
end
end
end
29 changes: 29 additions & 0 deletions lib/parallel_calabash/ios/patches.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class <<RunLoop::SimControl
def terminate_all_sims
puts "Patched terminate_all_sims"
end
end

class RunLoop::Instruments
INSTRUMENTS_FIND_PIDS_CMD = "ps x -o pid,command | grep -v grep | grep 'instruments -w #{ENV["DEVICE_TARGET"]}'"
end

class <<RunLoop::CoreSimulator
def quit_simulator
puts "Patched quit_simulator"
end
end

class RunLoop::SimControl
remove_method :ensure_accessibility, :ensure_software_keyboard, :quit_sim
def ensure_accessibility device
puts "patched ensure_accessibility"
end
def ensure_software_keyboard device
puts "patched ensure_software_keyboard"
end

def quit_sim opts={}
puts "Patched quit_sim"
end
end
90 changes: 90 additions & 0 deletions lib/parallel_calabash/ios/xcrun_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require 'run_loop'

module ParallelCalabash
module Ios
class XcrunHelper
def self.sim_name
run_loop = RunLoop::SimControl.new
if run_loop.xcode_version_gte_7?
'Simulator'
elsif run_loop.xcode_version_gte_6?
'iOS Simulator'
else
'iPhone Simulator'
end
end

def self.stop_and_remove(device_name, ssh)
devices = %x( #{ssh} "xcrun simctl list devices | grep \"#{device_name}\"" )
puts "Devices: #{devices}"
devices.each_line do |device|
_name, id, state = device.match(/^\s*([^(]*?)\s*\((\S+)\)\s+\((\S+)\)/).captures
self.try_stop_simulator id, state, ssh
puts 'Delete: ' + %x( #{ssh} "xcrun simctl delete #{id}")
end
end

def self.try_stop_simulator device_uuid, state, ssh
if state.downcase =~ /booted/
%x( #{ssh} "xcrun simctl shutdown #{device_uuid}")
pids = %x( #{ssh} "ps -e | grep #{device_uuid}")
if pids.match(self.sim_name)
app_pid = pids.lines.select {|line| line.match self.sim_name }.first.split(" ").first
%x(#{ssh} "kill -9 #{app_pid}")
puts "Shutdown: #{device_uuid}"
else
puts "Shutdown: #{device_uuid} failed, not present"
end
end
end

def self.create_simulator(device_name, ssh, simulator)
stop_and_remove(device_name, ssh)
device_info = %x( #{ssh} "xcrun simctl create '#{device_name}' #{simulator}" ).strip
fail "Failed to create #{device_name} for #{simulator}" unless device_info
device_info
end

def initialize env, is_device, device_target
@env = env
@is_device = is_device
@simulator_uuid = !@is_device ? device_target : nil
end

def start_simulator_and_app_if_needed
unless @is_device
%x( xcrun simctl boot '#{@simulator_uuid}' )
%x( xcrun simctl install '#{@simulator_uuid}' '#{@env[:APP_BUNDLE_PATH]}' )
%x( xcrun simctl shutdown '#{@simulator_uuid}' )
%x( xcrun open -n -g -a "#{XcrunHelper.sim_name}" --args -CurrentDeviceUDID #{@simulator_uuid} )
%x( mkdir -p ~/.parallel-loop/#{@simulator_uuid} )
launch_app
end
end

def launch_app
%x( instruments -w '#{@simulator_uuid}' \
-D '~/.parallel-loop/#{@simulator_uuid}/instrument.trace' \
-t Automation '#{@env[:APP_BUNDLE_PATH]}' \
-e UIASCRIPT '#{File.join(File.dirname(__FILE__),"../../../misc/startup_popup_close.js")}' \
-e UIARESULTSPATH ~/.parallel-loop/#{@simulator_uuid} \
>& ~/.parallel-loop/#{@simulator_uuid}/run-loop.out )
end

def set_env_vars_if_needed
unless @is_device
@env[:DEVICE_TARGET] = device_instruments_target(@simulator_uuid)
@env[:SIMULATOR_UUID] = @simulator_uuid
end
@env[:NO_LAUNCH] = '1'
end

private
def device_instruments_target device_target
device = %x( instruments -s devices | grep "#{device_target}" )
_name, platform, uuid = device.match(/([A-z0-9_]*)\s([(].*[)])\s(.*)/).captures
"#{_name} #{platform}"
end
end
end
end
50 changes: 27 additions & 23 deletions lib/parallel_calabash/runner.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'fileutils'
require 'find'
require 'parallel_calabash/ios/xcrun_helper'
require 'run_loop'

module ParallelCalabash
module Runner
Expand Down Expand Up @@ -97,7 +99,7 @@ def run_tests(test_files, process_number, options)

o = execute_command_for_process(process_number, test)
device = @device_helper.device_for_process process_number
log = "/tmp/PCal-#{device[:USER]}.process_number"
log = "/tmp/PCal-#{device[:USER]}.#{process_number}"
puts "Writing log #{log}"
open(log, 'w') { |file| file.print o[:stdout] }
o
Expand All @@ -109,20 +111,17 @@ def command_for_test(process_number, test_files, app_path, cucumber_options, sim
remote = device[:USER] ? "ssh #{device[:USER]}@localhost" : 'bash -c'

if device[:CALABASH_SERVER_PORT]
user_app = copy_app_set_port(app_path, device)
user_app = copy_app_set_port(app_path, device, device[:USER] || "PCal_app_#{process_number}")
else
user_app = app_path
end

device_name = device[:DEVICE_NAME] || "PCal-#{device[:USER]}"
device_simulator = device[:SIMULATOR] || simulator

# Device target changed in XCode 7, losing ' Simulator' for some reason.
maybe_simulator = @device_helper.xcode7? ? '' : ' Simulator'
device_target = device[:DEVICE_TARGET] || "#{device_name} (#{version(device_simulator)}#{maybe_simulator})"
device_info = device[:DEVICE_TARGET] || (device[:USER] ? create_simulator(device_name, remote, simulator) : '')
device_target = device[:DEVICE_TARGET] || ParallelCalabash::Ios::XcrunHelper.create_simulator(device_name, remote, "#{device_simulator}" )
device_endpoint = device[:DEVICE_ENDPOINT] || "http://localhost:#{device[:CALABASH_SERVER_PORT]}"
$stdout.print "#{process_number}>> Device: #{device_info} = #{device_name} = #{device_target}\n"
$stdout.print "#{process_number}>> Device: #{device_name} = #{device_target}\n"
$stdout.flush

unless @skip_ios_ping_check
Expand All @@ -137,12 +136,19 @@ def command_for_test(process_number, test_files, app_path, cucumber_options, sim
AUTOTEST: '1',
DEVICE_ENDPOINT: device_endpoint,
DEVICE_TARGET: device_target,
DEVICE_INFO: device_info,
TEST_USER: device[:USER] || %x( whoami ).strip,
# 'DEBUG_UNIX_CALLS' => '1',
TEST_PROCESS_NUMBER: (process_number+1).to_s,
SCREENSHOT_PATH: "PCal_#{process_number+1}_"
SCREENSHOT_PATH: "PCal_#{process_number+1}_",
APP_BUNDLE_PATH: user_app
}

unless device[:USER]
xcrun_helper = ParallelCalabash::Ios::XcrunHelper.new(env, device[:DEVICE], device_target)
xcrun_helper.set_env_vars_if_needed
xcrun_helper.start_simulator_and_app_if_needed
end

env['BUNDLE_ID'] = ENV['BUNDLE_ID'] if ENV['BUNDLE_ID']
exports = env.map { |k, v| WINDOWS ? "(SET \"#{k}=#{v}\")" : "#{k}='#{v}';export #{k}" }.join(separator)

Expand Down Expand Up @@ -179,6 +185,9 @@ def prepare_for_parallel_execution
FileUtils.chmod_R('g+w', 'build/reports') if File.exists? 'build/reports'
FileUtils.chmod('g+w', Dir['*'])
FileUtils.chmod('g+w', '.')

#remove run loop files if any
FileUtils.rm_rf("~/.parallel-loop")
end

def create_simulator(device_name, ssh, simulator)
Expand All @@ -202,23 +211,18 @@ def stop_and_remove(device_name, ssh)
end
end

def copy_app_set_port(app_path, device)
user_path = File.dirname(app_path) + '/' + device[:USER]
FileUtils.rmtree(user_path)
FileUtils.mkdir_p(user_path)
user_app = user_path + '/' + File.basename(app_path)
FileUtils.copy_entry(app_path, user_app)

# Set plist.
def copy_app_set_port(app_path, device, target_path)
process_path = File.dirname(app_path) + '/' + target_path
FileUtils.rmtree(process_path)
FileUtils.mkdir_p(process_path)
process_app = process_path + '/' + File.basename(app_path)
FileUtils.copy_entry(app_path, process_app)

system("/usr/libexec/PlistBuddy -c 'Delete CalabashServerPort integer #{device[:CALABASH_SERVER_PORT]}' #{user_app}/Info.plist")
unless system("/usr/libexec/PlistBuddy -c 'Add CalabashServerPort integer #{device[:CALABASH_SERVER_PORT]}' #{user_app}/Info.plist")
raise "Unable to set CalabashServerPort in #{user_app}/Info.plist"
end
RunLoop::PlistBuddy.new.plist_set("CalabashServerPort", "integer", device[:CALABASH_SERVER_PORT], "#{process_app}/Info.plist")

puts "User app: #{user_app}"
puts "Process app: #{process_app}"

user_app
process_app
end
end
end
2 changes: 1 addition & 1 deletion lib/parallel_calabash/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ParallelCalabash
VERSION = "0.2.4"
VERSION = "0.2.5"
end
7 changes: 7 additions & 0 deletions misc/startup_popup_close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
UIATarget.onAlert = function onAlert(alert) {
var title = alert.name();
UIALogger.logWarning("Alert with title '" + title + "' encountered.");
return true;
}
UIATarget.localTarget().delay(4);
UIATarget.localTarget().frontMostApp().alert().buttons()[0].tap();
1 change: 1 addition & 0 deletions parallel_calabash.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ Gem::Specification.new do |spec|

spec.add_development_dependency "bundler", "~> 1.6"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_runtime_dependency "run_loop", ">= 1.5"
spec.add_runtime_dependency 'parallel'
end