Skip to content

Commit 7d874e5

Browse files
committed
Implement repeat command
f
1 parent 69cdaaf commit 7d874e5

File tree

14 files changed

+305
-26
lines changed

14 files changed

+305
-26
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
name: Bug report
3+
about: Create a report to help us improve
4+
title: ''
5+
labels: ''
6+
assignees: ''
7+
8+
---
9+
10+
## What did you do?
11+
12+
13+
## What did you expect to happen?
14+
15+
16+
## What happened instead?
17+
18+
19+
## Environment
20+
21+
- `xcmonkey` version:
22+
- `idb` version:
23+
- `xcode` version:
24+
- `macOS` version:
25+
26+
## Additional context
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
name: Feature Request
3+
about: Got any ideas about new features? Let us know!
4+
title: ''
5+
labels: ''
6+
assignees: ''
7+
8+
---
9+
10+
## What are you trying to achieve?
11+
12+
13+
## If possible, how can you achieve this currently?
14+
15+
16+
## What would be the better way?
17+
18+
19+
## Environment
20+
21+
- `xcmonkey` version:
22+
- `idb` version:
23+
- `xcode` version:
24+
- `macOS` version:
25+
26+
## Additional context

.github/pull_request_template.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# Changes
22

33
## References
4+
45
- https://github.com/alteral/xcmonkey/issues/XXX
56

7+
## Description
8+
9+
610
## Risks
11+
712
- [ ] None
813
- [ ] Low
914
- [ ] High

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ DerivedData
3939

4040
# Sonar
4141
.scannerwork
42+
43+
# Cache
44+
xcmonkey-session.json

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
## Description
1313

14-
*xcmonkey* is a tool for doing randomised UI testing of iOS apps. It's inspired by and has similar goals to [*monkey*](https://developer.android.com/studio/test/monkey) on Android.
14+
*xcmonkey* is a tool for doing stress testing of iOS apps. It's inspired by and has similar goals to [*monkey*](https://developer.android.com/studio/test/monkey) on Android.
1515

1616
Under the hood, *xcmonkey* uses [iOS Development Bridge](https://fbidb.io/) as a driver, that's why it's pretty smart and can do a lot of things, such as taps, swipes and presses. All that comes «pseudo-random» because it has access to the screen hierarchy, and so can either do actions blindly (like tapping on random points) or precisely (like tapping on the existing elements).
1717

@@ -63,6 +63,33 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
6363
}
6464
```
6565

66+
### To repeat the stress test from generated session
67+
68+
```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+
```
92+
6693
### To describe the required point
6794

6895
```bash

bin/xcmonkey

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'commander/import'
44
require_relative '../lib/xcmonkey'
55
require_relative '../lib/xcmonkey/describer'
6+
require_relative '../lib/xcmonkey/repeater'
67
require_relative '../lib/xcmonkey/logger'
78
require_relative '../lib/xcmonkey/driver'
89
require_relative '../lib/xcmonkey/version'
@@ -18,25 +19,41 @@ module Xcmonkey
1819
c.option('-b', '--bundle-id STRING', String, 'Set target bundle identifier')
1920
c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds. Defaults to `60`')
2021
c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
21-
c.action do |args, options|
22-
options.default(duration: 60, enable_simulator_keyboard: true)
22+
c.option('-s', '--session-path STRING', String, 'Path where monkey testing session should be saved. Defaults to current directory')
23+
c.action do |_, options|
24+
options.default(
25+
duration: 60,
26+
session_path: Dir.pwd,
27+
enable_simulator_keyboard: true
28+
)
2329
params = {
2430
udid: options.udid,
2531
bundle_id: options.bundle_id,
2632
duration: options.duration,
27-
simulator_keyboard: options.enable_simulator_keyboard
33+
session_path: options.session_path,
34+
enable_simulator_keyboard: options.enable_simulator_keyboard
2835
}
2936
Xcmonkey.new(params).run
3037
end
3138
end
3239

40+
command :repeat do |c|
41+
c.syntax = 'xcmonkey repeat [options]'
42+
c.description = 'Repeats given session'
43+
c.option('-s', '--session-path STRING', String, 'Path to monkey testing session')
44+
c.action do |_, options|
45+
params = { session_path: options.session_path }
46+
Repeater.new(params).run
47+
end
48+
end
49+
3350
command :describe do |c|
3451
c.syntax = 'xcmonkey describe [options]'
3552
c.description = 'Describes given point'
3653
c.option('-u', '--udid STRING', String, 'Set device UDID')
3754
c.option('-x', '--x STRING', 'Point `x` coordinate')
3855
c.option('-y', '--y STRING', 'Point `y` coordinate')
39-
c.action do |args, options|
56+
c.action do |_, options|
4057
params = {
4158
udid: options.udid,
4259
x: options.x,

lib/xcmonkey.rb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
require 'json'
22
require 'colorize'
33
require_relative 'xcmonkey/describer'
4+
require_relative 'xcmonkey/repeater'
45
require_relative 'xcmonkey/version'
56
require_relative 'xcmonkey/logger'
67
require_relative 'xcmonkey/driver'
78

89
module Xcmonkey
910
class Xcmonkey
10-
attr_accessor :udid, :bundle_id, :duration, :driver
11+
attr_accessor :driver
1112

1213
def initialize(params)
1314
ensure_required_params(params)
14-
self.udid = params[:udid]
15-
self.bundle_id = params[:bundle_id]
16-
self.duration = params[:duration]
1715
self.driver = Driver.new(params)
1816
end
1917

2018
def run
21-
driver.ensure_device_exists
22-
driver.ensure_app_installed
23-
driver.terminate_app
24-
driver.open_home_screen(with_tracker: true)
25-
driver.launch_app
2619
driver.monkey_test(gestures)
2720
end
2821

@@ -35,7 +28,11 @@ def gestures
3528

3629
def ensure_required_params(params)
3730
Logger.error('UDID should be provided') if params[:udid].nil?
31+
3832
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
33+
34+
Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])
35+
3936
if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
4037
Logger.error('Duration must be Integer and not less than 1 second')
4138
end

lib/xcmonkey/describer.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
class Describer
2-
attr_accessor :udid, :x, :y, :driver
2+
attr_accessor :x, :y, :driver
33

44
def initialize(params)
55
ensure_required_params(params)
6-
self.udid = params[:udid]
76
self.x = params[:x]
87
self.y = params[:y]
98
self.driver = Driver.new(params)

lib/xcmonkey/driver.rb

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
class Driver
2-
attr_accessor :udid, :bundle_id, :duration, :enable_simulator_keyboard
2+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :session_duration, :session_path, :session_actions
33

44
def initialize(params)
55
self.udid = params[:udid]
66
self.bundle_id = params[:bundle_id]
7-
self.duration = params[:duration]
7+
self.session_duration = params[:duration]
8+
self.session_path = params[:session_path]
89
self.enable_simulator_keyboard = params[:enable_simulator_keyboard]
10+
self.session_actions = params[:session_actions]
11+
@session = { params: params, actions: [] }
912
ensure_driver_installed
1013
end
1114

15+
def monkey_test_precondition
16+
ensure_device_exists
17+
ensure_app_installed
18+
terminate_app
19+
open_home_screen(with_tracker: true)
20+
launch_app
21+
end
22+
1223
def monkey_test(gestures)
24+
monkey_test_precondition
1325
app_elements = describe_ui.shuffle
1426
current_time = Time.now
15-
while Time.now < current_time + duration
27+
while Time.now < current_time + session_duration
1628
el1_coordinates = central_coordinates(app_elements.first)
1729
el2_coordinates = central_coordinates(app_elements.last)
1830
case gestures.sample
@@ -40,7 +52,30 @@ def monkey_test(gestures)
4052
next
4153
end
4254
app_elements = describe_ui.shuffle
43-
Logger.error('App lost') if app_elements.include?(@home_tracker)
55+
next unless app_elements.include?(@home_tracker)
56+
57+
save_session
58+
Logger.error('App lost')
59+
end
60+
save_session
61+
end
62+
63+
def repeat_monkey_test
64+
monkey_test_precondition
65+
session_actions.each do |action|
66+
case action['type']
67+
when 'tap'
68+
tap(coordinates: { x: action['x'], y: action['y'] })
69+
when 'press'
70+
press(coordinates: { x: action['x'], y: action['y'] }, duration: action['duration'])
71+
when 'swipe'
72+
swipe(
73+
start_coordinates: { x: action['x'], y: action['y'] },
74+
end_coordinates: { x: action['endX'], y: action['endY'] },
75+
duration: action['duration']
76+
)
77+
end
78+
Logger.error('App lost') if describe_ui.shuffle.include?(@home_tracker)
4479
end
4580
end
4681

@@ -113,11 +148,13 @@ def list_apps
113148

114149
def tap(coordinates:)
115150
Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
151+
@session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
116152
`idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
117153
end
118154

119155
def press(coordinates:, duration:)
120156
Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
157+
@session[:actions] << { type: :press, x: coordinates[:x], y: coordinates[:y], duration: duration } unless session_actions
121158
`idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
122159
end
123160

@@ -126,6 +163,16 @@ def swipe(start_coordinates:, end_coordinates:, duration:)
126163
"Swipe (#{duration}s):",
127164
payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
128165
)
166+
unless session_actions
167+
@session[:actions] << {
168+
type: :swipe,
169+
x: start_coordinates[:x],
170+
y: start_coordinates[:y],
171+
endX: end_coordinates[:x],
172+
endY: end_coordinates[:y],
173+
duration: duration
174+
}
175+
end
129176
coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
130177
`idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
131178
end
@@ -168,6 +215,10 @@ def press_duration
168215
rand(0.5..1.5).ceil(1)
169216
end
170217

218+
def save_session
219+
File.write("#{session_path}/xcmonkey-session.json", JSON.pretty_generate(@session))
220+
end
221+
171222
private
172223

173224
def ensure_driver_installed

lib/xcmonkey/repeater.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class Repeater
2+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
3+
4+
def initialize(params)
5+
validate_session(params[:session_path])
6+
end
7+
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
17+
18+
def validate_session(session_path)
19+
Logger.error("Provided session can't be found: #{session_path}") unless File.exist?(session_path)
20+
21+
session = JSON.parse(File.read(session_path))
22+
23+
if session['params'].nil?
24+
Logger.error('Provided session is not valid: `params` should not be `nil`')
25+
return
26+
end
27+
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?
30+
31+
self.udid = session['params']['udid']
32+
Logger.error('Provided session is not valid: `udid` should not be `nil`') if udid.nil?
33+
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?
36+
37+
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
38+
end
39+
end

0 commit comments

Comments
 (0)