parallel_appium 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/.gitignore +457 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +68 -0
- data/LICENSE +674 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/apps/.gitignore +3 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/certs/javondavis.pem +27 -0
- data/docs/.gitignore +0 -0
- data/docs/_config.yml +1 -0
- data/exe/parallel_appium +4 -0
- data/lib/parallel_appium/android.rb +35 -0
- data/lib/parallel_appium/ios.rb +30 -0
- data/lib/parallel_appium/selenium-server-standalone-3.12.0.jar +0 -0
- data/lib/parallel_appium/server.rb +164 -0
- data/lib/parallel_appium/version.rb +3 -0
- data/lib/parallel_appium.rb +218 -0
- data/parallel_appium.gemspec +52 -0
- data.tar.gz.sig +0 -0
- metadata +200 -0
- metadata.gz.sig +0 -0
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# parallel_appium
|
2
|
+
|
3
|
+
Distributed mobile testing in Appium
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'parallel_appium'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install parallel_appium
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
28
|
+
|
29
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/JavonDavis/parallel_appium. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
34
|
+
|
35
|
+
## License
|
36
|
+
|
37
|
+
The gem is available as open source under the terms of the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
38
|
+
|
39
|
+
## Code of Conduct
|
40
|
+
|
41
|
+
Everyone interacting in the ParallelAppium project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/JavonDavis/parallel_appium/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/apps/.gitignore
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'parallel_appium'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIEhTCCAu2gAwIBAgIBATANBgkqhkiG9w0BAQsFADBEMRYwFAYDVQQDDA1qYXZv
|
3
|
+
bmxkYXZpczE0MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
|
4
|
+
FgNjb20wHhcNMTgwNjA2MTcxNjM2WhcNMTkwNjA2MTcxNjM2WjBEMRYwFAYDVQQD
|
5
|
+
DA1qYXZvbmxkYXZpczE0MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJ
|
6
|
+
k/IsZAEZFgNjb20wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDCwCJw
|
7
|
+
CoVPoQgQSDGGGRWyt40oMNZHFc85AiRjBU7UMFgYSPQIt0M+SCyJaptM0pJYGG65
|
8
|
+
M/hx4Hnqw93ELvPWtAAHgP/9u05K3uiWgsxFRHm5S9JxMS/nvNRatVYvy2hKQGTU
|
9
|
+
zJpczncAhu0Lj9tlCuZMdvnhh9PAInrNaXQMkkk4n/XV9dk3lhyqemMFyKJhS0dO
|
10
|
+
k3JTn3muVdSOdLMOGJtreHdV5TByA/rIYQGraIqHrlbAbnGOnNunMk53M9eI3zAq
|
11
|
+
gz8fydEkaHswv7Mbk3kWnN67UMaNUpoElnEEj0242BtXhXeXr9FnqcFLwUrQLXKK
|
12
|
+
kc2BjV1CbK7AHMKLfU2yBQJVqEkb+Xu8dAkpHP333adf7KEW8N0vf+639jgBPbxa
|
13
|
+
y25Sh4ZQAuue1NVl43Uj7Qkbb2V4epiNr1JhRrpvMSQah2lx9yNs/NtgdZWHtidd
|
14
|
+
+3chmKUroK4elci3NKa76G+iwbD/pJkWg0zFt4cg4+pgWdmjX3PLnd0iv2UCAwEA
|
15
|
+
AaOBgTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSjN7L+WZRc
|
16
|
+
/FBz01MZd7lsGI5j7jAiBgNVHREEGzAZgRdqYXZvbmxkYXZpczE0QGdtYWlsLmNv
|
17
|
+
bTAiBgNVHRIEGzAZgRdqYXZvbmxkYXZpczE0QGdtYWlsLmNvbTANBgkqhkiG9w0B
|
18
|
+
AQsFAAOCAYEAoxRokaBO+VS8xqmPtqEKofcXF2kJDemn9Jn9Begjj7mzqUVHOR9w
|
19
|
+
4HHaAY79m1xm+yiveWgOsHvb04GCjR32dJjAcbb7dsg3aXOd49UoQ0lOVtOigH2s
|
20
|
+
v5BqR5svvPKoDA8nP0V9pTOxFboJw5dtY+s95Py6mc6nTsK3XdCUS80PiY3xPWS+
|
21
|
+
pCzJWE1mNMhRndpb9E0BAiimdmo3suQo7EVYQ3tPsnHwaSdv52tt9kW+rpoHFRV5
|
22
|
+
gjyXjRGX/lW897Zs2AA7yFb7oVvbaAlGyH2MLVnB5aiTywqE0ZgD9CTgSnGYu0iY
|
23
|
+
28Uiprdx/AqGsAVNV8YAsPMEm/81xTXmJ0HRrpyvMHyAXuNPp84t4TsO2DQtAlrU
|
24
|
+
XuXlmerWWuxSKZOwV9fJfl+j5fO7bzUyeWK76xvvWGNtIF19xPnGdPpupaClWMHg
|
25
|
+
pjBkMT4ow4ZhVgBKPDT2jh74b8g2rIDSbkJIj0lf35WXqJqZzseabGPpTbtzJEap
|
26
|
+
HIrd7mPILLe3
|
27
|
+
-----END CERTIFICATE-----
|
data/docs/.gitignore
ADDED
File without changes
|
data/docs/_config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
theme: jekyll-theme-cayman
|
data/exe/parallel_appium
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module ParallelAppium
|
2
|
+
# Connecting to Android devices
|
3
|
+
class Android
|
4
|
+
# Fire up the Android emulators
|
5
|
+
def start_emulators
|
6
|
+
emulators = `emulator -list-avds`.split("\n")
|
7
|
+
emulators = emulators[0, ENV['THREADS'].to_i]
|
8
|
+
Parallel.map(emulators, in_threads: emulators.size) do |emulator|
|
9
|
+
spawn("emulator -avd #{emulator} -scale 100dpi -no-boot-anim -no-audio -accel on &", out: '/dev/null')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Get additional information for the Android device with unique identifier udid
|
14
|
+
def get_android_device_data(udid)
|
15
|
+
specs = { os: 'ro.build.version.release', manufacturer: 'ro.product.manufacturer', model: 'ro.product.model', sdk: 'ro.build.version.sdk' }
|
16
|
+
hash = {}
|
17
|
+
specs.each do |key, spec|
|
18
|
+
value = `adb -s #{udid} shell getprop "#{spec}"`.strip
|
19
|
+
hash.merge!(key => value.to_s)
|
20
|
+
end
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
|
24
|
+
# Devices after cleanup and supplemental data included
|
25
|
+
def devices
|
26
|
+
start_emulators
|
27
|
+
sleep 10
|
28
|
+
devices = `adb devices`.split("\n").select { |x| x.include? "\tdevice" }.map.each_with_index { |d, i| {platform: 'android', name: 'android', udid: d.split("\t")[0], wdaPort: 8100 + i, thread: i + 1} }
|
29
|
+
devices = devices.map { |x| x.merge(get_android_device_data(x[:udid])) }
|
30
|
+
|
31
|
+
ENV['DEVICES'] = JSON.generate(devices)
|
32
|
+
devices
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ParallelAppium
|
2
|
+
# Connecting to iOS devices
|
3
|
+
class IOS
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
# Get available simulators
|
7
|
+
@simulators = `instruments -s devices`.split("\n").reverse
|
8
|
+
end
|
9
|
+
|
10
|
+
# Filter simulator data
|
11
|
+
def simulator_information
|
12
|
+
re = /\([0-9]+\.[0-9]\) \[[0-9A-Z-]+\]/m
|
13
|
+
|
14
|
+
# Filter out simulator info for iPhone platform version and udid
|
15
|
+
@simulators.select { |simulator_data| simulator_data.include?('iPhone') && !simulator_data.include?('Apple Watch') }
|
16
|
+
.map { |simulator_data| simulator_data.scan(re)[0].tr('()[]', '').split }[0, ENV['THREADS'].to_i]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Devices after cleanup and supplemental data included
|
20
|
+
def devices
|
21
|
+
devices = []
|
22
|
+
simulator_information.each_with_index do |data, i|
|
23
|
+
devices.push(name: @simulators[i][0, @simulators[i].index('(') - 1], platform: 'ios', os: data[0], udid: data[1],
|
24
|
+
wdaPort: 8100 + i + ENV['THREADS'].to_i, thread: i + 1)
|
25
|
+
end
|
26
|
+
ENV['DEVICES'] = JSON.generate(devices)
|
27
|
+
devices
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
Binary file
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module ParallelAppium
|
2
|
+
# Setting up the selenium grid server
|
3
|
+
class Server
|
4
|
+
# Sets the current thread number environment variable(TEST_ENV_NUMBER)
|
5
|
+
def thread
|
6
|
+
(ENV['TEST_ENV_NUMBER'].nil? || ENV['TEST_ENV_NUMBER'].empty? ? 1 : ENV['TEST_ENV_NUMBER']).to_i
|
7
|
+
end
|
8
|
+
|
9
|
+
# Get the device data from the DEVICES environment variable
|
10
|
+
def device_data
|
11
|
+
JSON.parse(ENV['DEVICES']).find { |t| t['thread'].eql? thread } unless ENV['DEVICES'].nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Save device specifications to output directory
|
15
|
+
def save_device_data(dev_array)
|
16
|
+
dev_array.each do |device|
|
17
|
+
device_hash = {}
|
18
|
+
device.each do |key, value|
|
19
|
+
device_hash[key] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
# Delete and create output folder
|
23
|
+
`rm -rf output`
|
24
|
+
`mkdir output`
|
25
|
+
|
26
|
+
device.each do |k, v|
|
27
|
+
open("output/specs-#{device_hash[:udid]}.log", 'a') do |file|
|
28
|
+
file << "#{k}: #{v}\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set UDID and name environment variable
|
35
|
+
def set_udid_environment_variable
|
36
|
+
ENV['UDID'] = device_data['udid'] unless device_data.nil?
|
37
|
+
ENV['name'] = device_data['name'] unless device_data.nil? # Unique on ios but could be repeated on android
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the device information for the respective platform
|
41
|
+
def get_devices(platform)
|
42
|
+
ENV['THREADS'] = '1' if ENV['THREADS'].nil?
|
43
|
+
if platform == 'android'
|
44
|
+
Android.new.devices
|
45
|
+
elsif platform == 'ios'
|
46
|
+
IOS.new.devices
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Start the appium server with the specified options
|
51
|
+
def appium_server_start(**options)
|
52
|
+
command = +'appium'
|
53
|
+
command << " --nodeconfig #{options[:config]}" if options.key?(:config)
|
54
|
+
command << " -p #{options[:port]}" if options.key?(:port)
|
55
|
+
command << " -bp #{options[:bp]}" if options.key?(:bp)
|
56
|
+
command << " --log #{Dir.pwd}/output/#{options[:log]}" if options.key?(:log)
|
57
|
+
command << " --tmp #{ENV['BASE_DIR']}/tmp/#{options[:tmp]}" if options.key?(:tmp)
|
58
|
+
Dir.chdir('.') do
|
59
|
+
puts(command)
|
60
|
+
pid = spawn(command, out: '/dev/null')
|
61
|
+
puts 'Waiting for Appium to start up...'
|
62
|
+
sleep 10
|
63
|
+
puts "Appium PID: #{pid}"
|
64
|
+
puts 'Appium server did not start' if pid.nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate node config for sellenium grid
|
69
|
+
def generate_node_config(file_name, appium_port, device)
|
70
|
+
system 'mkdir node_configs >> /dev/null 2>&1'
|
71
|
+
f = File.new("#{Dir.pwd}/node_configs/#{file_name}", 'w')
|
72
|
+
f.write(JSON.generate(
|
73
|
+
capabilities: [{ browserName: device[:udid], maxInstances: 5, platform: device[:platform] }],
|
74
|
+
configuration: { cleanUpCycle: 2000,
|
75
|
+
timeout: 1_800_000,
|
76
|
+
registerCycle: 5000,
|
77
|
+
proxy: 'org.openqa.grid.selenium.proxy.DefaultRemoteProxy',
|
78
|
+
url: "http://127.0.0.1:#{appium_port}/wd/hub",
|
79
|
+
host: '127.0.0.1',
|
80
|
+
port: appium_port,
|
81
|
+
maxSession: 5,
|
82
|
+
register: true,
|
83
|
+
hubPort: 4444,
|
84
|
+
hubHost: 'localhost' }
|
85
|
+
))
|
86
|
+
f.close
|
87
|
+
end
|
88
|
+
|
89
|
+
# Start the Selenium grid server as a hub
|
90
|
+
def start_hub
|
91
|
+
spawn("java -jar #{File.dirname(__FILE__)}/selenium-server-standalone-3.12.0.jar -role hub -newSessionWaitTimeout 250000 -log #{Dir.pwd}/output/hub.log &", out: '/dev/null')
|
92
|
+
sleep 3 # wait for hub to start...
|
93
|
+
spawn('open -a safari http://127.0.0.1:4444/grid/console')
|
94
|
+
end
|
95
|
+
|
96
|
+
# Start an appium server or the platform on the specified port
|
97
|
+
def start_single_appium(platform, port)
|
98
|
+
puts 'Getting Device data'
|
99
|
+
devices = get_devices(platform)[0]
|
100
|
+
if devices.nil?
|
101
|
+
puts "No devices for #{platform}, Exiting..."
|
102
|
+
exit
|
103
|
+
else
|
104
|
+
udid = devices[:udid]
|
105
|
+
save_device_data [devices]
|
106
|
+
end
|
107
|
+
ENV['UDID'] = udid
|
108
|
+
appium_server_start udid: udid, log: "appium-#{udid}.log", port: port
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check if a port on an ip address is available
|
112
|
+
def port_open?(ip, port)
|
113
|
+
begin
|
114
|
+
Timeout.timeout(1) do
|
115
|
+
begin
|
116
|
+
s = TCPSocket.new(ip, port)
|
117
|
+
s.close
|
118
|
+
return true
|
119
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
rescue Timeout::Error
|
124
|
+
return false
|
125
|
+
end
|
126
|
+
false
|
127
|
+
end
|
128
|
+
|
129
|
+
# Launch the Selenium grid hub and required appium instances
|
130
|
+
def launch_hub_and_nodes(platform)
|
131
|
+
start_hub unless port_open?('localhost', 4444)
|
132
|
+
devices = get_devices(platform)
|
133
|
+
|
134
|
+
if devices.nil?
|
135
|
+
puts "No devices for #{platform}, Exiting...."
|
136
|
+
exit
|
137
|
+
else
|
138
|
+
save_device_data [devices]
|
139
|
+
end
|
140
|
+
|
141
|
+
threads = ENV['THREADS'].to_i
|
142
|
+
if devices.size < threads
|
143
|
+
puts "Not enough available devices, reducing to #{devices.size} threads"
|
144
|
+
ENV['THREADS'] = devices.size.to_s
|
145
|
+
else
|
146
|
+
puts "Using #{threads} of the available #{devices.size} devices"
|
147
|
+
devices = devices[0, threads]
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
Parallel.map_with_index(devices, in_processes: devices.size) do |device, index|
|
152
|
+
offset = platform == 'android' ? 0 : threads
|
153
|
+
port = 4000 + index + offset
|
154
|
+
bp = 2250 + index + offset
|
155
|
+
config_name = "#{device[:udid]}.json"
|
156
|
+
generate_node_config config_name, port, device
|
157
|
+
node_config = "#{Dir.pwd}/node_configs/#{config_name}"
|
158
|
+
puts port
|
159
|
+
appium_server_start config: node_config, port: port, bp: bp, udid: device[:udid],
|
160
|
+
log: "appium-#{device[:udid]}.log", tmp: device[:udid]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'parallel_appium/version'
|
2
|
+
require 'parallel_appium/server'
|
3
|
+
require 'parallel_appium/android'
|
4
|
+
require 'parallel_appium/ios'
|
5
|
+
require 'parallel_tests'
|
6
|
+
require 'parallel'
|
7
|
+
require 'appium_lib'
|
8
|
+
require 'socket'
|
9
|
+
require 'timeout'
|
10
|
+
require 'json'
|
11
|
+
|
12
|
+
module ParallelAppium
|
13
|
+
# Set up environment, Selenium and Appium
|
14
|
+
class ParallelAppium
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@server = Server.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Kill process by pattern name
|
21
|
+
def kill_process(process)
|
22
|
+
`ps -ef | grep #{process} | awk '{print $2}' | xargs kill -9 >> /dev/null 2>&1`
|
23
|
+
end
|
24
|
+
|
25
|
+
# Load capabilities based on current device data
|
26
|
+
def load_capabilities(caps)
|
27
|
+
device = @server.device_data
|
28
|
+
unless device.nil?
|
29
|
+
caps[:caps][:udid] = device.fetch('udid', nil)
|
30
|
+
caps[:caps][:platformVersion] = device.fetch('os', caps[:caps][:platformVersion])
|
31
|
+
caps[:caps][:deviceName] = device.fetch('name', caps[:caps][:deviceName])
|
32
|
+
caps[:caps][:wdaLocalPort] = device.fetch('wdaPort', nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
caps[:caps][:sessionOverride] = true
|
36
|
+
caps[:caps][:useNewWDA] = true
|
37
|
+
# TODO: Optionally set these capabilities below
|
38
|
+
caps[:caps][:noReset] = true
|
39
|
+
caps[:caps][:fullReset] = false
|
40
|
+
caps[:appium_lib][:server_url] = ENV['SERVER_URL']
|
41
|
+
caps
|
42
|
+
end
|
43
|
+
|
44
|
+
# Load appium text file if available and attempt to start the driver
|
45
|
+
# platform is either android or ios, otherwise read from ENV
|
46
|
+
# caps is mapping of appium capabilities
|
47
|
+
def initialize_appium(**args)
|
48
|
+
platform = args[:platform]
|
49
|
+
caps = args[:caps]
|
50
|
+
|
51
|
+
platform = ENV['platform'] if platform.nil?
|
52
|
+
|
53
|
+
if platform.nil?
|
54
|
+
puts 'Platform not found in environment variable'
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
|
58
|
+
caps = Appium.load_appium_txt file: File.new("#{Dir.pwd}/appium-#{platform}.txt") if caps.nil?
|
59
|
+
|
60
|
+
if caps.nil?
|
61
|
+
puts 'No capabilities specified'
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
capabilities = load_capabilities(caps)
|
65
|
+
@driver = Appium::Driver.new(capabilities, true)
|
66
|
+
@driver.start_driver
|
67
|
+
Appium.promote_appium_methods Object
|
68
|
+
Appium.promote_appium_methods RSpec::Core::ExampleGroup
|
69
|
+
@driver
|
70
|
+
end
|
71
|
+
|
72
|
+
# Define a signal handler for SIGINT
|
73
|
+
def setup_signal_handler(ios_pid = nil, android_pid = nil)
|
74
|
+
Signal.trap('INT') do
|
75
|
+
Process.kill('INT', ios_pid) unless ios_pid.nil?
|
76
|
+
Process.kill('INT', android_pid) unless android_pid.nil?
|
77
|
+
|
78
|
+
# Kill any existing Appium and Selenium processes
|
79
|
+
kill_process 'appium'
|
80
|
+
kill_process 'selenium'
|
81
|
+
|
82
|
+
# Terminate ourself
|
83
|
+
exit 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Decide whether to execute specs in parallel or not
|
88
|
+
# @param [String] platform
|
89
|
+
# @param [int] threads
|
90
|
+
# @param [String] spec_path
|
91
|
+
# @param [boolean] parallel
|
92
|
+
def execute_specs(platform, threads, spec_path, parallel = false)
|
93
|
+
command = if parallel
|
94
|
+
"platform=#{platform} parallel_rspec -n #{threads} #{spec_path} > output/#{platform}.log"
|
95
|
+
else
|
96
|
+
"platform=#{platform} rspec #{spec_path} --tag #{platform} > output/#{platform}.log"
|
97
|
+
end
|
98
|
+
|
99
|
+
puts "Executing #{command}"
|
100
|
+
exec command
|
101
|
+
end
|
102
|
+
|
103
|
+
# Define Spec path, validate platform and execute specs
|
104
|
+
def setup(platform, file_path, threads, parallel)
|
105
|
+
spec_path = 'spec/'
|
106
|
+
spec_path = file_path.to_s unless file_path.nil?
|
107
|
+
puts "SPEC PATH:#{spec_path}"
|
108
|
+
|
109
|
+
unless %w[ios android].include? platform
|
110
|
+
puts "Invalid platform #{platform}"
|
111
|
+
exit
|
112
|
+
end
|
113
|
+
|
114
|
+
execute_specs platform, threads, spec_path, parallel
|
115
|
+
end
|
116
|
+
|
117
|
+
# Validate platform is valid
|
118
|
+
def check_platform(platform)
|
119
|
+
options = %w[ios android all]
|
120
|
+
if platform.nil?
|
121
|
+
puts 'No platform detected... Options: ios,android,all'
|
122
|
+
exit
|
123
|
+
elsif !options.include? platform.downcase
|
124
|
+
puts "Invalid platform #{platform}"
|
125
|
+
exit
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Fire necessary appium server instances and Selenium grid server if needed.
|
130
|
+
def start(**args)
|
131
|
+
|
132
|
+
platform = args[:platform]
|
133
|
+
file_path = args[:file_path]
|
134
|
+
|
135
|
+
# Validate environment variable
|
136
|
+
if ENV['platform'].nil?
|
137
|
+
if platform.nil?
|
138
|
+
puts 'No platform detected in environment and none passed to start...'
|
139
|
+
exit
|
140
|
+
end
|
141
|
+
ENV['platform'] = platform
|
142
|
+
end
|
143
|
+
|
144
|
+
sleep 3
|
145
|
+
|
146
|
+
# Appium ports
|
147
|
+
ios_port = 4725
|
148
|
+
android_port = 4727
|
149
|
+
default_port = 4725
|
150
|
+
|
151
|
+
platform = ENV['platform']
|
152
|
+
|
153
|
+
# Platform is required
|
154
|
+
check_platform platform
|
155
|
+
|
156
|
+
platform = platform.downcase
|
157
|
+
ENV['BASE_DIR'] = Dir.pwd
|
158
|
+
|
159
|
+
# Check if multithreaded for distributing tests across devices
|
160
|
+
threads = ENV['THREADS'].nil? ? 1 : ENV['THREADS'].to_i
|
161
|
+
parallel = threads != 1
|
162
|
+
|
163
|
+
if platform != 'all'
|
164
|
+
pid = fork do
|
165
|
+
if !parallel
|
166
|
+
ENV['SERVER_URL'] = "http://0.0.0.0:#{default_port}/wd/hub"
|
167
|
+
@server.start_single_appium platform, default_port
|
168
|
+
else
|
169
|
+
ENV['SERVER_URL'] = 'http://localhost:4444/wd/hub'
|
170
|
+
@server.launch_hub_and_nodes platform
|
171
|
+
end
|
172
|
+
setup(platform, file_path, threads, parallel)
|
173
|
+
end
|
174
|
+
|
175
|
+
puts "PID: #{pid}"
|
176
|
+
setup_signal_handler(pid)
|
177
|
+
Process.waitpid(pid)
|
178
|
+
else # Spin off 2 sub-processes, one for Android connections and another for iOS,
|
179
|
+
# each with redefining environment variables for the server url, number of threads and platform
|
180
|
+
ios_pid = fork do
|
181
|
+
ENV['THREADS'] = threads.to_s
|
182
|
+
if parallel
|
183
|
+
ENV['SERVER_URL'] = 'http://localhost:4444/wd/hub'
|
184
|
+
puts 'Start iOS'
|
185
|
+
@server.launch_hub_and_nodes 'ios'
|
186
|
+
else
|
187
|
+
ENV['SERVER_URL'] = "http://0.0.0.0:#{ios_port}/wd/hub"
|
188
|
+
puts 'Start iOS'
|
189
|
+
@server.start_single_appium 'ios', ios_port
|
190
|
+
end
|
191
|
+
setup('ios', file_path, threads, parallel)
|
192
|
+
end
|
193
|
+
|
194
|
+
android_pid = fork do
|
195
|
+
ENV['THREADS'] = threads.to_s
|
196
|
+
if parallel
|
197
|
+
ENV['SERVER_URL'] = 'http://localhost:4444/wd/hub'
|
198
|
+
puts 'Start Android'
|
199
|
+
@server.launch_hub_and_nodes 'android'
|
200
|
+
else
|
201
|
+
ENV['SERVER_URL'] = "http://0.0.0.0:#{android_port}/wd/hub"
|
202
|
+
puts 'Start Android'
|
203
|
+
@server.start_single_appium 'android', android_port
|
204
|
+
end
|
205
|
+
setup('android', file_path, threads, parallel)
|
206
|
+
end
|
207
|
+
|
208
|
+
puts "iOS PID: #{ios_pid}\nAndroid PID: #{android_pid}"
|
209
|
+
setup_signal_handler(ios_pid, android_pid)
|
210
|
+
[ios_pid, android_pid].each { |process_pid| Process.waitpid(process_pid) }
|
211
|
+
end
|
212
|
+
|
213
|
+
# Kill any existing Appium and Selenium processes
|
214
|
+
kill_process 'appium'
|
215
|
+
kill_process 'selenium'
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path('lib', __dir__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'parallel_appium/version'
|
5
|
+
|
6
|
+
summary = 'Start multi-threaded appium servers'
|
7
|
+
description = 'Through the creation of multiple environments
|
8
|
+
this library allows for the distribution of multiple tests across 1 or more
|
9
|
+
devices or simulators. The library uses Selenium Grid and Appium to launch a
|
10
|
+
specified number of web driver and appium instances to spread the test load
|
11
|
+
across available devices'
|
12
|
+
|
13
|
+
Gem::Specification.new do |spec|
|
14
|
+
spec.name = 'parallel_appium'
|
15
|
+
spec.version = ParallelAppium::VERSION
|
16
|
+
spec.authors = ['Javon Davis']
|
17
|
+
spec.email = ['javonldavis14@gmail.com']
|
18
|
+
|
19
|
+
spec.summary = summary
|
20
|
+
spec.description = description
|
21
|
+
spec.homepage = 'https://github.com/JavonDavis/Parallel_Appium'
|
22
|
+
spec.license = 'GPL-3.0'
|
23
|
+
|
24
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
25
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
26
|
+
# if spec.respond_to?(:metadata)
|
27
|
+
# spec.metadata['allowed_push_host'] = "http://mygemserver.com"
|
28
|
+
# else
|
29
|
+
# raise 'RubyGems 2.4.1 or newer is required to protect against ' \
|
30
|
+
# 'public gem pushes.'
|
31
|
+
# end
|
32
|
+
|
33
|
+
# Specify which files should be added to the gem when it is released.
|
34
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
35
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
36
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
37
|
+
end
|
38
|
+
spec.bindir = 'exe'
|
39
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
40
|
+
spec.require_paths = ['lib']
|
41
|
+
|
42
|
+
spec.cert_chain = ['certs/javondavis.pem']
|
43
|
+
spec.signing_key = File.expand_path('~/.ssh/gem-private_key.pem') if $PROGRAM_NAME =~ /gem\z/
|
44
|
+
|
45
|
+
spec.add_development_dependency 'appium_lib', '~> 9.14'
|
46
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
47
|
+
spec.add_development_dependency 'json', '~> 1.8'
|
48
|
+
spec.add_development_dependency 'parallel', '~> 1.12'
|
49
|
+
spec.add_development_dependency 'parallel_tests', '~> 2.21'
|
50
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
51
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
52
|
+
end
|
data.tar.gz.sig
ADDED
Binary file
|