Skip to content

Commit

Permalink
Support fastlane integration (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
testableapple authored Jan 20, 2023
1 parent 40b1a42 commit 38e0bfa
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 218 deletions.
92 changes: 42 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,32 @@ gem 'xcmonkey'
### To run a stress test

```bash
$ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
12:44:19.343: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock
xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100

12:44:19.343: Device info: {
"name": "iPhone 14 Pro",
"udid": "413EA256-CFFB-4312-94A6-12592BEE4CBA",
"state": "Booted",
"type": "simulator",
"os_version": "iOS 16.2",
"architecture": "x86_64",
"path": "/tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock",
"is_local": true,
"companion": "/tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock"
}

12:44:22.550: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=74636
12:44:22.550: App info: {
"bundle_id": "com.apple.Maps",
"name": "Maps",
"install_type": "system",
"architectures": [
"x86_64",
"arm64"
],
"process_state": "Running",
"debuggable": false,
"pid": "49186"
}

12:44:23.203: Tap: {
"x": 53,
Expand All @@ -66,59 +88,29 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
### To repeat the stress test from generated session

```bash
$ xcmonkey repeat --session-path "./xcmonkey-session.json"
12:48:13.333: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock

12:48:16.542: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=73416

12:48:20.195: Tap: {
"x": 53,
"y": 749
}

12:48:20.404: Swipe (0.5s): {
"x": 196,
"y": 426
} => {
"x": 143,
"y": 447
}

12:48:21.155: Press (1.2s): {
"x": 143,
"y": 323
}
xcmonkey repeat --session-path "./xcmonkey-session.json"
```

### To describe the required point

```bash
$ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
20:05:20.212: Device info: iPhone 14 Pro | 413EA256-CFFB-4312-94A6-12592BEE4CBA | Booted | simulator | iOS 16.2 | x86_64 | /tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock

20:05:21.713: x:20 y:625 point info: {
"AXFrame": "{{19, 624.3}, {86, 130.6}}",
"AXUniqueId": "ShortcutsRowCell",
"frame": {
"y": 624.3,
"x": 19,
"width": 86,
"height": 130.6
},
"role_description": "button",
"AXLabel": "Home",
"content_required": false,
"type": "Button",
"title": null,
"help": null,
"custom_actions": [
xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
```

],
"AXValue": "Add",
"enabled": true,
"role": "AXButton",
"subrole": null
}
## [fastlane](https://github.com/fastlane/fastlane) integration

To run *xcmonkey* from *fastlane*, add the following code to your `Fastfile`:

```ruby
require 'xcmonkey'

lane :test do
Xcmonkey.new(
udid: '413EA256-CFFB-4312-94A6-12592BEE4CBA',
bundle_id: 'com.apple.Maps',
duration: 100
).run
end
```

## Code of Conduct
Expand Down
7 changes: 1 addition & 6 deletions bin/xcmonkey
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require_relative '../lib/xcmonkey/logger'
require_relative '../lib/xcmonkey/driver'
require_relative '../lib/xcmonkey/version'

module Xcmonkey
class Xcmonkey
program :version, VERSION
program :description, 'xcmonkey is a tool for doing randomised UI testing of iOS apps'

Expand All @@ -21,11 +21,6 @@ module Xcmonkey
c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
c.option('-s', '--session-path STRING', String, 'Path where monkey testing session should be saved. Defaults to current directory')
c.action do |_, options|
options.default(
duration: 60,
session_path: Dir.pwd,
enable_simulator_keyboard: true
)
params = {
udid: options.udid,
bundle_id: options.bundle_id,
Expand Down
51 changes: 26 additions & 25 deletions lib/xcmonkey.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,37 @@
require_relative 'xcmonkey/logger'
require_relative 'xcmonkey/driver'

module Xcmonkey
class Xcmonkey
attr_accessor :driver
class Xcmonkey
attr_accessor :driver

def initialize(params)
ensure_required_params(params)
self.driver = Driver.new(params)
end
def initialize(params)
params[:session_path] = Dir.pwd if params[:session_path].nil?
params[:duration] = 60 if params[:duration].nil?
params[:enable_simulator_keyboard] = true if params[:enable_simulator_keyboard].nil?
ensure_required_params(params)
self.driver = Driver.new(params)
end

def run
driver.monkey_test(gestures)
end
def run
driver.monkey_test(gestures)
end

def gestures
taps = [:precise_tap, :blind_tap] * 10
swipes = [:precise_swipe, :blind_swipe] * 5
presses = [:precise_press, :blind_press]
taps + swipes + presses
end
def gestures
taps = [:precise_tap, :blind_tap] * 10
swipes = [:precise_swipe, :blind_swipe] * 5
presses = [:precise_press, :blind_press]
taps + swipes + presses
end

def ensure_required_params(params)
Logger.error('UDID should be provided') if params[:udid].nil?
def ensure_required_params(params)
Logger.error('UDID should be provided') if params[:udid].nil?

Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?

Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])
Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])

if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
Logger.error('Duration must be Integer and not less than 1 second')
end
end
end
if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
Logger.error('Duration must be Integer and not less than 1 second')
end
end
end
32 changes: 16 additions & 16 deletions lib/xcmonkey/describer.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
class Describer
attr_accessor :x, :y, :driver
attr_accessor :x, :y, :driver

def initialize(params)
ensure_required_params(params)
self.x = params[:x]
self.y = params[:y]
self.driver = Driver.new(params)
end
def initialize(params)
ensure_required_params(params)
self.x = params[:x]
self.y = params[:y]
self.driver = Driver.new(params)
end

def run
driver.ensure_device_exists
driver.describe_point(x, y)
end
def run
driver.ensure_device_exists
driver.describe_point(x, y)
end

def ensure_required_params(params)
Logger.error('UDID should be provided') if params[:udid].nil?
Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
end
def ensure_required_params(params)
Logger.error('UDID should be provided') if params[:udid].nil?
Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
end
end
36 changes: 17 additions & 19 deletions lib/xcmonkey/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(params)
end

def monkey_test_precondition
puts
ensure_device_exists
ensure_app_installed
terminate_app
Expand Down Expand Up @@ -121,33 +122,31 @@ def configure_simulator_keyboard
end

def list_targets
@list_targets ||= `idb list-targets`.split("\n")
@list_targets
@targets ||= `idb list-targets --json`.split("\n").map! { |target| JSON.parse(target) }
@targets
end

def list_booted_simulators
`idb list-targets`.split("\n").grep(/Booted/)
def list_apps
`idb list-apps --udid #{udid} --json`.split("\n").map! { |app| JSON.parse(app) }
end

def ensure_app_installed
Logger.error("App #{bundle_id} is not installed on device #{udid}") unless list_apps.include?(bundle_id)
return if list_apps.any? { |app| app['bundle_id'] == bundle_id }

Logger.error("App #{bundle_id} is not installed on device #{udid}")
end

def ensure_device_exists
device = list_targets.detect { |target| target.include?(udid) }
device = list_targets.detect { |target| target['udid'] == udid }
Logger.error("Can't find device #{udid}") if device.nil?

Logger.info('Device info:', payload: device)
if device.include?('simulator')
Logger.info('Device info:', payload: JSON.pretty_generate(device))
if device['type'] == 'simulator'
configure_simulator_keyboard
boot_simulator
end
end

def list_apps
`idb list-apps --udid #{udid}`
end

def tap(coordinates:)
Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
@session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
Expand Down Expand Up @@ -236,14 +235,13 @@ def detect_home_unique_element
end

def wait_until_app_launched
app_info = nil
app_is_running = false
current_time = Time.now
while app_info.nil? && Time.now < current_time + 5
app_info = list_apps.split("\n").detect do |app|
app =~ /#{bundle_id}.*Running/
end
while !app_is_running && Time.now < current_time + 5
app_info = list_apps.detect { |app| app['bundle_id'] == bundle_id }
app_is_running = app_info && app_info['process_state'] == 'Running'
end
Logger.error("Can't run the app #{bundle_id}") if app_info.nil?
Logger.info('App info:', payload: app_info)
Logger.error("Can't run the app #{bundle_id}") unless app_is_running
Logger.info('App info:', payload: JSON.pretty_generate(app_info))
end
end
56 changes: 28 additions & 28 deletions lib/xcmonkey/repeater.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
class Repeater
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions

def initialize(params)
validate_session(params[:session_path])
end
def initialize(params)
validate_session(params[:session_path])
end

def run
params = {
udid: udid,
bundle_id: bundle_id,
enable_simulator_keyboard: enable_simulator_keyboard,
session_actions: actions
}
Driver.new(params).repeat_monkey_test
end
def run
params = {
udid: udid,
bundle_id: bundle_id,
enable_simulator_keyboard: enable_simulator_keyboard,
session_actions: actions
}
Driver.new(params).repeat_monkey_test
end

def validate_session(session_path)
Logger.error("Provided session can't be found: #{session_path}") unless File.exist?(session_path)
def validate_session(session_path)
Logger.error("Provided session can't be found: #{session_path}") unless File.exist?(session_path)

session = JSON.parse(File.read(session_path))
session = JSON.parse(File.read(session_path))

if session['params'].nil?
Logger.error('Provided session is not valid: `params` should not be `nil`')
return
end
if session['params'].nil?
Logger.error('Provided session is not valid: `params` should not be `nil`')
return
end

self.actions = session['actions']
Logger.error('Provided session is not valid: `actions` should not be `nil` or `empty`') if actions.nil? || actions.empty?
self.actions = session['actions']
Logger.error('Provided session is not valid: `actions` should not be `nil` or `empty`') if actions.nil? || actions.empty?

self.udid = session['params']['udid']
Logger.error('Provided session is not valid: `udid` should not be `nil`') if udid.nil?
self.udid = session['params']['udid']
Logger.error('Provided session is not valid: `udid` should not be `nil`') if udid.nil?

self.bundle_id = session['params']['bundle_id']
Logger.error('Provided session is not valid: `bundle_id` should not be `nil`') if bundle_id.nil?
self.bundle_id = session['params']['bundle_id']
Logger.error('Provided session is not valid: `bundle_id` should not be `nil`') if bundle_id.nil?

self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
end
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
end
end
5 changes: 2 additions & 3 deletions lib/xcmonkey/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
module Xcmonkey
VERSION = '1.0.0'
GEM_NAME = 'xcmonkey'
class Xcmonkey
VERSION = '1.1.0'
end
Loading

0 comments on commit 38e0bfa

Please sign in to comment.