Skip to content

Commit 38e0bfa

Browse files
Support fastlane integration (#8)
1 parent 40b1a42 commit 38e0bfa

File tree

10 files changed

+192
-218
lines changed

10 files changed

+192
-218
lines changed

README.md

Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,32 @@ gem 'xcmonkey'
3939
### To run a stress test
4040

4141
```bash
42-
$ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
43-
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
42+
xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
43+
44+
12:44:19.343: Device info: {
45+
"name": "iPhone 14 Pro",
46+
"udid": "413EA256-CFFB-4312-94A6-12592BEE4CBA",
47+
"state": "Booted",
48+
"type": "simulator",
49+
"os_version": "iOS 16.2",
50+
"architecture": "x86_64",
51+
"path": "/tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock",
52+
"is_local": true,
53+
"companion": "/tmp/idb/413EA256-CFFB-4312-94A6-12592BEE4CBA_companion.sock"
54+
}
4455

45-
12:44:22.550: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=74636
56+
12:44:22.550: App info: {
57+
"bundle_id": "com.apple.Maps",
58+
"name": "Maps",
59+
"install_type": "system",
60+
"architectures": [
61+
"x86_64",
62+
"arm64"
63+
],
64+
"process_state": "Running",
65+
"debuggable": false,
66+
"pid": "49186"
67+
}
4668

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

6890
```bash
69-
$ xcmonkey repeat --session-path "./xcmonkey-session.json"
70-
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
71-
72-
12:48:16.542: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=73416
73-
74-
12:48:20.195: Tap: {
75-
"x": 53,
76-
"y": 749
77-
}
78-
79-
12:48:20.404: Swipe (0.5s): {
80-
"x": 196,
81-
"y": 426
82-
} => {
83-
"x": 143,
84-
"y": 447
85-
}
86-
87-
12:48:21.155: Press (1.2s): {
88-
"x": 143,
89-
"y": 323
90-
}
91+
xcmonkey repeat --session-path "./xcmonkey-session.json"
9192
```
9293

9394
### To describe the required point
9495

9596
```bash
96-
$ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
97-
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
98-
99-
20:05:21.713: x:20 y:625 point info: {
100-
"AXFrame": "{{19, 624.3}, {86, 130.6}}",
101-
"AXUniqueId": "ShortcutsRowCell",
102-
"frame": {
103-
"y": 624.3,
104-
"x": 19,
105-
"width": 86,
106-
"height": 130.6
107-
},
108-
"role_description": "button",
109-
"AXLabel": "Home",
110-
"content_required": false,
111-
"type": "Button",
112-
"title": null,
113-
"help": null,
114-
"custom_actions": [
97+
xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
98+
```
11599

116-
],
117-
"AXValue": "Add",
118-
"enabled": true,
119-
"role": "AXButton",
120-
"subrole": null
121-
}
100+
## [fastlane](https://github.com/fastlane/fastlane) integration
101+
102+
To run *xcmonkey* from *fastlane*, add the following code to your `Fastfile`:
103+
104+
```ruby
105+
require 'xcmonkey'
106+
107+
lane :test do
108+
Xcmonkey.new(
109+
udid: '413EA256-CFFB-4312-94A6-12592BEE4CBA',
110+
bundle_id: 'com.apple.Maps',
111+
duration: 100
112+
).run
113+
end
122114
```
123115

124116
## Code of Conduct

bin/xcmonkey

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require_relative '../lib/xcmonkey/logger'
88
require_relative '../lib/xcmonkey/driver'
99
require_relative '../lib/xcmonkey/version'
1010

11-
module Xcmonkey
11+
class Xcmonkey
1212
program :version, VERSION
1313
program :description, 'xcmonkey is a tool for doing randomised UI testing of iOS apps'
1414

@@ -21,11 +21,6 @@ module Xcmonkey
2121
c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
2222
c.option('-s', '--session-path STRING', String, 'Path where monkey testing session should be saved. Defaults to current directory')
2323
c.action do |_, options|
24-
options.default(
25-
duration: 60,
26-
session_path: Dir.pwd,
27-
enable_simulator_keyboard: true
28-
)
2924
params = {
3025
udid: options.udid,
3126
bundle_id: options.bundle_id,

lib/xcmonkey.rb

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,37 @@
66
require_relative 'xcmonkey/logger'
77
require_relative 'xcmonkey/driver'
88

9-
module Xcmonkey
10-
class Xcmonkey
11-
attr_accessor :driver
9+
class Xcmonkey
10+
attr_accessor :driver
1211

13-
def initialize(params)
14-
ensure_required_params(params)
15-
self.driver = Driver.new(params)
16-
end
12+
def initialize(params)
13+
params[:session_path] = Dir.pwd if params[:session_path].nil?
14+
params[:duration] = 60 if params[:duration].nil?
15+
params[:enable_simulator_keyboard] = true if params[:enable_simulator_keyboard].nil?
16+
ensure_required_params(params)
17+
self.driver = Driver.new(params)
18+
end
1719

18-
def run
19-
driver.monkey_test(gestures)
20-
end
20+
def run
21+
driver.monkey_test(gestures)
22+
end
2123

22-
def gestures
23-
taps = [:precise_tap, :blind_tap] * 10
24-
swipes = [:precise_swipe, :blind_swipe] * 5
25-
presses = [:precise_press, :blind_press]
26-
taps + swipes + presses
27-
end
24+
def gestures
25+
taps = [:precise_tap, :blind_tap] * 10
26+
swipes = [:precise_swipe, :blind_swipe] * 5
27+
presses = [:precise_press, :blind_press]
28+
taps + swipes + presses
29+
end
2830

29-
def ensure_required_params(params)
30-
Logger.error('UDID should be provided') if params[:udid].nil?
31+
def ensure_required_params(params)
32+
Logger.error('UDID should be provided') if params[:udid].nil?
3133

32-
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
34+
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
3335

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

36-
if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
37-
Logger.error('Duration must be Integer and not less than 1 second')
38-
end
39-
end
40-
end
38+
if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
39+
Logger.error('Duration must be Integer and not less than 1 second')
40+
end
41+
end
4142
end

lib/xcmonkey/describer.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
class Describer
2-
attr_accessor :x, :y, :driver
2+
attr_accessor :x, :y, :driver
33

4-
def initialize(params)
5-
ensure_required_params(params)
6-
self.x = params[:x]
7-
self.y = params[:y]
8-
self.driver = Driver.new(params)
9-
end
4+
def initialize(params)
5+
ensure_required_params(params)
6+
self.x = params[:x]
7+
self.y = params[:y]
8+
self.driver = Driver.new(params)
9+
end
1010

11-
def run
12-
driver.ensure_device_exists
13-
driver.describe_point(x, y)
14-
end
11+
def run
12+
driver.ensure_device_exists
13+
driver.describe_point(x, y)
14+
end
1515

16-
def ensure_required_params(params)
17-
Logger.error('UDID should be provided') if params[:udid].nil?
18-
Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
19-
Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
20-
end
16+
def ensure_required_params(params)
17+
Logger.error('UDID should be provided') if params[:udid].nil?
18+
Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
19+
Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
20+
end
2121
end

lib/xcmonkey/driver.rb

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def initialize(params)
1313
end
1414

1515
def monkey_test_precondition
16+
puts
1617
ensure_device_exists
1718
ensure_app_installed
1819
terminate_app
@@ -121,33 +122,31 @@ def configure_simulator_keyboard
121122
end
122123

123124
def list_targets
124-
@list_targets ||= `idb list-targets`.split("\n")
125-
@list_targets
125+
@targets ||= `idb list-targets --json`.split("\n").map! { |target| JSON.parse(target) }
126+
@targets
126127
end
127128

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

132133
def ensure_app_installed
133-
Logger.error("App #{bundle_id} is not installed on device #{udid}") unless list_apps.include?(bundle_id)
134+
return if list_apps.any? { |app| app['bundle_id'] == bundle_id }
135+
136+
Logger.error("App #{bundle_id} is not installed on device #{udid}")
134137
end
135138

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

140-
Logger.info('Device info:', payload: device)
141-
if device.include?('simulator')
143+
Logger.info('Device info:', payload: JSON.pretty_generate(device))
144+
if device['type'] == 'simulator'
142145
configure_simulator_keyboard
143146
boot_simulator
144147
end
145148
end
146149

147-
def list_apps
148-
`idb list-apps --udid #{udid}`
149-
end
150-
151150
def tap(coordinates:)
152151
Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
153152
@session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
@@ -236,14 +235,13 @@ def detect_home_unique_element
236235
end
237236

238237
def wait_until_app_launched
239-
app_info = nil
238+
app_is_running = false
240239
current_time = Time.now
241-
while app_info.nil? && Time.now < current_time + 5
242-
app_info = list_apps.split("\n").detect do |app|
243-
app =~ /#{bundle_id}.*Running/
244-
end
240+
while !app_is_running && Time.now < current_time + 5
241+
app_info = list_apps.detect { |app| app['bundle_id'] == bundle_id }
242+
app_is_running = app_info && app_info['process_state'] == 'Running'
245243
end
246-
Logger.error("Can't run the app #{bundle_id}") if app_info.nil?
247-
Logger.info('App info:', payload: app_info)
244+
Logger.error("Can't run the app #{bundle_id}") unless app_is_running
245+
Logger.info('App info:', payload: JSON.pretty_generate(app_info))
248246
end
249247
end

lib/xcmonkey/repeater.rb

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
class Repeater
2-
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
2+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
33

4-
def initialize(params)
5-
validate_session(params[:session_path])
6-
end
4+
def initialize(params)
5+
validate_session(params[:session_path])
6+
end
77

8-
def run
9-
params = {
10-
udid: udid,
11-
bundle_id: bundle_id,
12-
enable_simulator_keyboard: enable_simulator_keyboard,
13-
session_actions: actions
14-
}
15-
Driver.new(params).repeat_monkey_test
16-
end
8+
def run
9+
params = {
10+
udid: udid,
11+
bundle_id: bundle_id,
12+
enable_simulator_keyboard: enable_simulator_keyboard,
13+
session_actions: actions
14+
}
15+
Driver.new(params).repeat_monkey_test
16+
end
1717

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

21-
session = JSON.parse(File.read(session_path))
21+
session = JSON.parse(File.read(session_path))
2222

23-
if session['params'].nil?
24-
Logger.error('Provided session is not valid: `params` should not be `nil`')
25-
return
26-
end
23+
if session['params'].nil?
24+
Logger.error('Provided session is not valid: `params` should not be `nil`')
25+
return
26+
end
2727

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

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

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

37-
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
38-
end
37+
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
38+
end
3939
end

lib/xcmonkey/version.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
module Xcmonkey
2-
VERSION = '1.0.0'
3-
GEM_NAME = 'xcmonkey'
1+
class Xcmonkey
2+
VERSION = '1.1.0'
43
end

0 commit comments

Comments
 (0)