xcmonkey 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/{issue_template → ISSUE_TEMPLATE}/bug_report.md +0 -0
- data/.github/{issue_template/feature_request.md → ISSUE_TEMPLATE/feature-request.md} +1 -1
- data/.github/workflows/test.yml +3 -2
- data/README.md +42 -50
- data/bin/xcmonkey +1 -6
- data/lib/xcmonkey/describer.rb +16 -16
- data/lib/xcmonkey/driver.rb +80 -46
- data/lib/xcmonkey/repeater.rb +28 -28
- data/lib/xcmonkey/version.rb +2 -3
- data/lib/xcmonkey.rb +26 -25
- data/spec/describer_spec.rb +4 -2
- data/spec/driver_spec.rb +112 -34
- data/spec/repeater_spec.rb +0 -1
- data/spec/xcmonkey_spec.rb +53 -55
- data/xcmonkey.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c93c94403332c085a393877d88d354b2736788f6e52d118eb8b9faa70c91b1e5
|
4
|
+
data.tar.gz: ee0f3bfcd014151b4821a6df50f601205581ebd480b5626e33588bda2928981a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ab88db343b0e83363130a9e9c8ed965acbf9d58f56d597b301ff12da4df9d4e79b08c3b2284b870a65c9bfc3d05a8ba9df0d19409631620a28836c9d5785814
|
7
|
+
data.tar.gz: fe46af5c215273be9210f3c880617457a12c6063e0bd1901d6abbd10750f10935b77a4797ff374926c3f1cac6b1ef79032f210fb705e05917146e4c64cede97c
|
File without changes
|
data/.github/workflows/test.yml
CHANGED
@@ -17,10 +17,11 @@ jobs:
|
|
17
17
|
env:
|
18
18
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
19
19
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
20
|
+
PR_NUMBER: ${{ github.event.number }}
|
20
21
|
steps:
|
21
|
-
- uses: actions/checkout@v3.
|
22
|
+
- uses: actions/checkout@v3.3.0
|
22
23
|
|
23
|
-
- uses: actions/setup-python@v4.
|
24
|
+
- uses: actions/setup-python@v4.5.0
|
24
25
|
with:
|
25
26
|
python-version: 3.11
|
26
27
|
cache: 'pip'
|
data/README.md
CHANGED
@@ -39,10 +39,32 @@ gem 'xcmonkey'
|
|
39
39
|
### To run a stress test
|
40
40
|
|
41
41
|
```bash
|
42
|
-
$ xcmonkey test --
|
43
|
-
|
42
|
+
$ xcmonkey test --duration 100 --bundle-id "com.apple.Maps" --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
|
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
|
+
}
|
44
55
|
|
45
|
-
12:44:22.550: App info:
|
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
|
+
}
|
46
68
|
|
47
69
|
12:44:23.203: Tap: {
|
48
70
|
"x": 53,
|
@@ -66,59 +88,29 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
|
|
66
88
|
### To repeat the stress test from generated session
|
67
89
|
|
68
90
|
```bash
|
69
|
-
|
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"
|
91
92
|
```
|
92
93
|
|
93
94
|
### To describe the required point
|
94
95
|
|
95
96
|
```bash
|
96
|
-
|
97
|
-
|
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
|
+
```
|
115
99
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
+
duration: 100,
|
110
|
+
bundle_id: 'com.apple.Maps',
|
111
|
+
udid: '413EA256-CFFB-4312-94A6-12592BEE4CBA'
|
112
|
+
).run
|
113
|
+
end
|
122
114
|
```
|
123
115
|
|
124
116
|
## Code of Conduct
|
data/bin/xcmonkey
CHANGED
@@ -8,7 +8,7 @@ require_relative '../lib/xcmonkey/logger'
|
|
8
8
|
require_relative '../lib/xcmonkey/driver'
|
9
9
|
require_relative '../lib/xcmonkey/version'
|
10
10
|
|
11
|
-
|
11
|
+
class Xcmonkey
|
12
12
|
program :version, VERSION
|
13
13
|
program :description, 'xcmonkey is a tool for doing randomised UI testing of iOS apps'
|
14
14
|
|
@@ -21,11 +21,6 @@ module Xcmonkey
|
|
21
21
|
c.option('-k', '--enable-simulator-keyboard', 'Should simulator keyboard be enabled? Defaults to `true`')
|
22
22
|
c.option('-s', '--session-path STRING', String, 'Path where monkey testing session should be saved. Defaults to current directory')
|
23
23
|
c.action do |_, options|
|
24
|
-
options.default(
|
25
|
-
duration: 60,
|
26
|
-
session_path: Dir.pwd,
|
27
|
-
enable_simulator_keyboard: true
|
28
|
-
)
|
29
24
|
params = {
|
30
25
|
udid: options.udid,
|
31
26
|
bundle_id: options.bundle_id,
|
data/lib/xcmonkey/describer.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
class Describer
|
2
|
-
|
2
|
+
attr_accessor :x, :y, :driver
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
def run
|
12
|
+
driver.ensure_device_exists
|
13
|
+
driver.describe_point(x, y)
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
21
21
|
end
|
data/lib/xcmonkey/driver.rb
CHANGED
@@ -13,17 +13,19 @@ class Driver
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def monkey_test_precondition
|
16
|
+
puts
|
16
17
|
ensure_device_exists
|
17
18
|
ensure_app_installed
|
18
|
-
terminate_app
|
19
|
-
|
20
|
-
|
19
|
+
terminate_app(bundle_id)
|
20
|
+
launch_app(target_bundle_id: bundle_id, wait_for_state_update: true)
|
21
|
+
@running_apps = list_running_apps
|
21
22
|
end
|
22
23
|
|
23
24
|
def monkey_test(gestures)
|
24
25
|
monkey_test_precondition
|
25
26
|
app_elements = describe_ui.shuffle
|
26
27
|
current_time = Time.now
|
28
|
+
counter = 0
|
27
29
|
while Time.now < current_time + session_duration
|
28
30
|
el1_coordinates = central_coordinates(app_elements.first)
|
29
31
|
el2_coordinates = central_coordinates(app_elements.last)
|
@@ -51,17 +53,17 @@ class Driver
|
|
51
53
|
else
|
52
54
|
next
|
53
55
|
end
|
56
|
+
detect_app_state_change
|
57
|
+
track_running_apps if counter % 5 == 0 # Track running apps after every 5th action to speed up the test
|
58
|
+
counter += 1
|
54
59
|
app_elements = describe_ui.shuffle
|
55
|
-
next unless app_elements.include?(@home_tracker)
|
56
|
-
|
57
|
-
save_session
|
58
|
-
Logger.error('App lost')
|
59
60
|
end
|
60
61
|
save_session
|
61
62
|
end
|
62
63
|
|
63
64
|
def repeat_monkey_test
|
64
65
|
monkey_test_precondition
|
66
|
+
counter = 0
|
65
67
|
session_actions.each do |action|
|
66
68
|
case action['type']
|
67
69
|
when 'tap'
|
@@ -74,16 +76,15 @@ class Driver
|
|
74
76
|
end_coordinates: { x: action['endX'], y: action['endY'] },
|
75
77
|
duration: action['duration']
|
76
78
|
)
|
79
|
+
else
|
80
|
+
next
|
77
81
|
end
|
78
|
-
|
82
|
+
detect_app_state_change
|
83
|
+
track_running_apps if counter % 5 == 0
|
84
|
+
counter += 1
|
79
85
|
end
|
80
86
|
end
|
81
87
|
|
82
|
-
def open_home_screen(with_tracker: false)
|
83
|
-
`idb ui button --udid #{udid} HOME`
|
84
|
-
detect_home_unique_element if with_tracker
|
85
|
-
end
|
86
|
-
|
87
88
|
def describe_ui
|
88
89
|
JSON.parse(`idb ui describe-all --udid #{udid}`)
|
89
90
|
end
|
@@ -94,13 +95,13 @@ class Driver
|
|
94
95
|
point_info
|
95
96
|
end
|
96
97
|
|
97
|
-
def launch_app
|
98
|
-
`idb launch --udid #{udid} #{
|
99
|
-
wait_until_app_launched
|
98
|
+
def launch_app(target_bundle_id:, wait_for_state_update: false)
|
99
|
+
`idb launch --udid #{udid} #{target_bundle_id}`
|
100
|
+
wait_until_app_launched(target_bundle_id) if wait_for_state_update
|
100
101
|
end
|
101
102
|
|
102
|
-
def terminate_app
|
103
|
-
`idb terminate --udid #{udid} #{
|
103
|
+
def terminate_app(target_bundle_id)
|
104
|
+
`idb terminate --udid #{udid} #{target_bundle_id} 2>/dev/null`
|
104
105
|
end
|
105
106
|
|
106
107
|
def boot_simulator
|
@@ -119,33 +120,38 @@ class Driver
|
|
119
120
|
end
|
120
121
|
|
121
122
|
def list_targets
|
122
|
-
@
|
123
|
-
@
|
123
|
+
@targets ||= `idb list-targets --json`.split("\n").map! { |target| JSON.parse(target) }
|
124
|
+
@targets
|
125
|
+
end
|
126
|
+
|
127
|
+
def list_apps
|
128
|
+
`idb list-apps --udid #{udid} --json`.split("\n").map! { |app| JSON.parse(app) }
|
124
129
|
end
|
125
130
|
|
126
|
-
def
|
127
|
-
|
131
|
+
def list_running_apps
|
132
|
+
list_apps.select { |app| app['process_state'] == 'Running' }
|
128
133
|
end
|
129
134
|
|
130
135
|
def ensure_app_installed
|
131
|
-
|
136
|
+
return if list_apps.any? { |app| app['bundle_id'] == bundle_id }
|
137
|
+
|
138
|
+
Logger.error("App #{bundle_id} is not installed on device #{udid}")
|
132
139
|
end
|
133
140
|
|
134
141
|
def ensure_device_exists
|
135
|
-
device = list_targets.detect { |target| target
|
142
|
+
device = list_targets.detect { |target| target['udid'] == udid }
|
136
143
|
Logger.error("Can't find device #{udid}") if device.nil?
|
137
144
|
|
138
|
-
Logger.info('Device info:', payload: device)
|
139
|
-
if device
|
145
|
+
Logger.info('Device info:', payload: JSON.pretty_generate(device))
|
146
|
+
if device['type'] == 'simulator'
|
140
147
|
configure_simulator_keyboard
|
141
148
|
boot_simulator
|
149
|
+
else
|
150
|
+
Logger.error('xcmonkey does not support real devices yet. ' \
|
151
|
+
'For more information see https://github.com/alteral/xcmonkey/issues/7')
|
142
152
|
end
|
143
153
|
end
|
144
154
|
|
145
|
-
def list_apps
|
146
|
-
`idb list-apps --udid #{udid}`
|
147
|
-
end
|
148
|
-
|
149
155
|
def tap(coordinates:)
|
150
156
|
Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
|
151
157
|
@session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
|
@@ -219,29 +225,57 @@ class Driver
|
|
219
225
|
File.write("#{session_path}/xcmonkey-session.json", JSON.pretty_generate(@session))
|
220
226
|
end
|
221
227
|
|
222
|
-
|
228
|
+
# This function takes ≈200ms
|
229
|
+
def track_running_apps
|
230
|
+
current_list_of_running_apps = list_running_apps
|
231
|
+
if @running_apps != current_list_of_running_apps
|
232
|
+
currently_running_bundle_ids = current_list_of_running_apps.map { |app| app['bundle_id'] }
|
233
|
+
previously_running_bundle_ids = @running_apps.map { |app| app['bundle_id'] }
|
234
|
+
new_apps = currently_running_bundle_ids - previously_running_bundle_ids
|
223
235
|
|
224
|
-
|
225
|
-
|
236
|
+
return if new_apps.empty?
|
237
|
+
|
238
|
+
launch_app(target_bundle_id: bundle_id)
|
239
|
+
new_apps.each do |id|
|
240
|
+
Logger.warn("Shutting down: #{id}")
|
241
|
+
terminate_app(id)
|
242
|
+
end
|
243
|
+
end
|
226
244
|
end
|
227
245
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
246
|
+
# This function takes ≈300ms
|
247
|
+
def detect_app_state_change
|
248
|
+
return unless detect_app_in_background
|
249
|
+
|
250
|
+
target_app_is_running = list_running_apps.any? { |app| app['bundle_id'] == bundle_id }
|
251
|
+
|
252
|
+
if target_app_is_running
|
253
|
+
launch_app(target_bundle_id: bundle_id)
|
254
|
+
else
|
255
|
+
save_session
|
256
|
+
Logger.error("Target app has crashed or been terminated")
|
232
257
|
end
|
233
|
-
@home_tracker
|
234
258
|
end
|
235
259
|
|
236
|
-
def
|
237
|
-
|
260
|
+
def detect_app_in_background
|
261
|
+
current_app_label = describe_ui.detect { |el| el['type'] == 'Application' }['AXLabel']
|
262
|
+
current_app_label.nil? || current_app_label.strip.empty?
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
def ensure_driver_installed
|
268
|
+
Logger.error("'idb' doesn't seem to be installed") if `which idb`.strip.empty?
|
269
|
+
end
|
270
|
+
|
271
|
+
def wait_until_app_launched(target_bundle_id)
|
272
|
+
app_is_running = false
|
238
273
|
current_time = Time.now
|
239
|
-
while
|
240
|
-
app_info = list_apps.
|
241
|
-
|
242
|
-
end
|
274
|
+
while !app_is_running && Time.now < current_time + 5
|
275
|
+
app_info = list_apps.detect { |app| app['bundle_id'] == target_bundle_id }
|
276
|
+
app_is_running = app_info && app_info['process_state'] == 'Running'
|
243
277
|
end
|
244
|
-
Logger.error("Can't run the app #{
|
245
|
-
Logger.info('App info:', payload: app_info)
|
278
|
+
Logger.error("Can't run the app #{target_bundle_id}") unless app_is_running
|
279
|
+
Logger.info('App info:', payload: JSON.pretty_generate(app_info))
|
246
280
|
end
|
247
281
|
end
|
data/lib/xcmonkey/repeater.rb
CHANGED
@@ -1,39 +1,39 @@
|
|
1
1
|
class Repeater
|
2
|
-
|
2
|
+
attr_accessor :udid, :bundle_id, :enable_simulator_keyboard, :actions
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
def initialize(params)
|
5
|
+
validate_session(params[:session_path])
|
6
|
+
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
17
|
|
18
|
-
|
19
|
-
|
18
|
+
def validate_session(session_path)
|
19
|
+
Logger.error("Provided session can't be found: #{session_path}") unless File.exist?(session_path)
|
20
20
|
|
21
|
-
|
21
|
+
session = JSON.parse(File.read(session_path))
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
if session['params'].nil?
|
24
|
+
Logger.error('Provided session is not valid: `params` should not be `nil`')
|
25
|
+
return
|
26
|
+
end
|
27
27
|
|
28
|
-
|
29
|
-
|
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
30
|
|
31
|
-
|
32
|
-
|
31
|
+
self.udid = session['params']['udid']
|
32
|
+
Logger.error('Provided session is not valid: `udid` should not be `nil`') if udid.nil?
|
33
33
|
|
34
|
-
|
35
|
-
|
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
36
|
|
37
|
-
|
38
|
-
|
37
|
+
self.enable_simulator_keyboard = session['params']['enable_simulator_keyboard']
|
38
|
+
end
|
39
39
|
end
|
data/lib/xcmonkey/version.rb
CHANGED
data/lib/xcmonkey.rb
CHANGED
@@ -6,36 +6,37 @@ require_relative 'xcmonkey/version'
|
|
6
6
|
require_relative 'xcmonkey/logger'
|
7
7
|
require_relative 'xcmonkey/driver'
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
attr_accessor :driver
|
9
|
+
class Xcmonkey
|
10
|
+
attr_accessor :driver
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
def run
|
21
|
+
driver.monkey_test(gestures)
|
22
|
+
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
+
def ensure_required_params(params)
|
32
|
+
Logger.error('UDID should be provided') if params[:udid].nil?
|
31
33
|
|
32
|
-
|
34
|
+
Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
|
33
35
|
|
34
|
-
|
36
|
+
Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
41
42
|
end
|
data/spec/describer_spec.rb
CHANGED
@@ -2,15 +2,17 @@ describe Describer do
|
|
2
2
|
let(:udid) { `xcrun simctl list | grep " iPhone 14 Pro Max"`.split("\n")[0].split('(')[1].split(')')[0] }
|
3
3
|
let(:driver) { Driver.new(udid: udid) }
|
4
4
|
|
5
|
-
|
5
|
+
before do
|
6
6
|
allow(Logger).to receive(:info)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'verifies that point can be described (integer)' do
|
7
10
|
driver.boot_simulator
|
8
11
|
point_info = described_class.new(udid: udid, x: 10, y: 10).run
|
9
12
|
expect(point_info).not_to be_empty
|
10
13
|
end
|
11
14
|
|
12
15
|
it 'verifies that point can be described (string)' do
|
13
|
-
allow(Logger).to receive(:info)
|
14
16
|
driver.boot_simulator
|
15
17
|
point_info = described_class.new(udid: udid, x: '10', y: '10').run
|
16
18
|
expect(point_info).not_to be_empty
|
data/spec/driver_spec.rb
CHANGED
@@ -4,36 +4,22 @@ describe Driver do
|
|
4
4
|
let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id, session_path: Dir.pwd) }
|
5
5
|
let(:driver_with_session) { described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ type: 'tap', x: 0, y: 0 }]) }
|
6
6
|
|
7
|
+
before do
|
8
|
+
allow(Logger).to receive(:info)
|
9
|
+
end
|
10
|
+
|
7
11
|
it 'verifies that sumulator was booted' do
|
8
12
|
error_message = "Failed to boot #{udid}"
|
9
13
|
expect(Logger).not_to receive(:error).with(error_message, payload: nil)
|
10
14
|
expect { driver.boot_simulator }.not_to raise_error
|
11
15
|
end
|
12
16
|
|
13
|
-
it 'verifies that there are booted simulators' do
|
14
|
-
driver.boot_simulator
|
15
|
-
booted_simulators = driver.list_booted_simulators
|
16
|
-
expect(booted_simulators).not_to be_empty
|
17
|
-
end
|
18
|
-
|
19
17
|
it 'verifies that ui can be described' do
|
20
18
|
driver.boot_simulator
|
21
19
|
ui = driver.describe_ui
|
22
20
|
expect(ui).not_to be_empty
|
23
21
|
end
|
24
22
|
|
25
|
-
it 'verifies that home screen can be opened' do
|
26
|
-
driver.boot_simulator
|
27
|
-
home_tracker = driver.open_home_screen(with_tracker: true)
|
28
|
-
expect(home_tracker).not_to be_empty
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'verifies that home screen can be opened without tracker' do
|
32
|
-
driver.boot_simulator
|
33
|
-
home_tracker = driver.open_home_screen(with_tracker: false)
|
34
|
-
expect(home_tracker).to be_nil
|
35
|
-
end
|
36
|
-
|
37
23
|
it 'verifies that list of targets can be showed' do
|
38
24
|
list_targets = driver.list_targets
|
39
25
|
expect(list_targets).not_to be_empty
|
@@ -41,8 +27,8 @@ describe Driver do
|
|
41
27
|
|
42
28
|
it 'verifies that list of apps can be showed' do
|
43
29
|
driver.boot_simulator
|
44
|
-
|
45
|
-
expect(
|
30
|
+
app_exists = driver.list_apps.any? { |app| app['bundle_id'] == bundle_id }
|
31
|
+
expect(app_exists).to be(true)
|
46
32
|
end
|
47
33
|
|
48
34
|
it 'verifies that app installed' do
|
@@ -62,11 +48,9 @@ describe Driver do
|
|
62
48
|
end
|
63
49
|
|
64
50
|
it 'verifies that device exists' do
|
65
|
-
|
66
|
-
payload = driver.list_targets.detect { |target| target.include?(udid) }
|
67
|
-
expect(Logger).not_to receive(:error).with(error_message, payload: nil)
|
68
|
-
expect(Logger).to receive(:info).with('Device info:', payload: payload)
|
51
|
+
expect(Logger).not_to receive(:error)
|
69
52
|
expect(driver).to receive(:boot_simulator)
|
53
|
+
expect(driver).to receive(:configure_simulator_keyboard)
|
70
54
|
expect { driver.ensure_device_exists }.not_to raise_error
|
71
55
|
end
|
72
56
|
|
@@ -143,18 +127,25 @@ describe Driver do
|
|
143
127
|
expect(keyboard_state.first).to include('1')
|
144
128
|
end
|
145
129
|
|
146
|
-
it 'verifies that app can be launched' do
|
130
|
+
it 'verifies that app can be launched with waiting' do
|
131
|
+
expect(Logger).not_to receive(:error)
|
132
|
+
expect(driver).to receive(:wait_until_app_launched)
|
133
|
+
driver.boot_simulator
|
134
|
+
driver.terminate_app(bundle_id)
|
135
|
+
expect { driver.launch_app(target_bundle_id: bundle_id, wait_for_state_update: true) }.not_to raise_error
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'verifies that app can be launched without waiting' do
|
147
139
|
expect(Logger).not_to receive(:error)
|
148
|
-
expect(
|
140
|
+
expect(driver).not_to receive(:wait_until_app_launched)
|
149
141
|
driver.boot_simulator
|
150
|
-
driver.terminate_app
|
151
|
-
expect { driver.launch_app }.not_to raise_error
|
142
|
+
driver.terminate_app(bundle_id)
|
143
|
+
expect { driver.launch_app(target_bundle_id: bundle_id) }.not_to raise_error
|
152
144
|
end
|
153
145
|
|
154
146
|
it 'verifies tap in new session' do
|
155
147
|
driver.boot_simulator
|
156
148
|
coordinates = { x: 1, y: 1 }
|
157
|
-
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
158
149
|
driver.tap(coordinates: coordinates)
|
159
150
|
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
160
151
|
end
|
@@ -162,7 +153,6 @@ describe Driver do
|
|
162
153
|
it 'verifies tap in old session' do
|
163
154
|
driver_with_session.boot_simulator
|
164
155
|
coordinates = { x: 1, y: 1 }
|
165
|
-
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
166
156
|
driver_with_session.tap(coordinates: coordinates)
|
167
157
|
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
168
158
|
end
|
@@ -171,7 +161,6 @@ describe Driver do
|
|
171
161
|
driver.boot_simulator
|
172
162
|
duration = 0.5
|
173
163
|
coordinates = { x: 1, y: 1 }
|
174
|
-
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
175
164
|
driver.press(coordinates: coordinates, duration: duration)
|
176
165
|
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
177
166
|
end
|
@@ -180,7 +169,6 @@ describe Driver do
|
|
180
169
|
driver_with_session.boot_simulator
|
181
170
|
duration = 0.5
|
182
171
|
coordinates = { x: 1, y: 1 }
|
183
|
-
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
184
172
|
driver_with_session.press(coordinates: coordinates, duration: duration)
|
185
173
|
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
186
174
|
end
|
@@ -190,7 +178,6 @@ describe Driver do
|
|
190
178
|
duration = 0.5
|
191
179
|
start_coordinates = { x: 1, y: 1 }
|
192
180
|
end_coordinates = { x: 2, y: 2 }
|
193
|
-
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
194
181
|
driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
195
182
|
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
196
183
|
end
|
@@ -200,7 +187,6 @@ describe Driver do
|
|
200
187
|
duration = 0.5
|
201
188
|
start_coordinates = { x: 1, y: 1 }
|
202
189
|
end_coordinates = { x: 2, y: 2 }
|
203
|
-
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
204
190
|
driver_with_session.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
205
191
|
expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
|
206
192
|
end
|
@@ -211,6 +197,98 @@ describe Driver do
|
|
211
197
|
driver.save_session
|
212
198
|
end
|
213
199
|
|
200
|
+
it 'verifies that monkey_test_precondition works fine' do
|
201
|
+
driver.monkey_test_precondition
|
202
|
+
app_info = driver.list_apps.detect { |app| app['bundle_id'] == bundle_id }
|
203
|
+
app_is_running = app_info && app_info['process_state'] == 'Running'
|
204
|
+
expect(app_is_running).to be(true)
|
205
|
+
expect(driver.instance_variable_get(:@running_apps)).not_to be_nil
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'verifies that monkey_test works fine' do
|
209
|
+
params = { udid: udid, bundle_id: bundle_id, duration: 1, session_path: Dir.pwd }
|
210
|
+
driver = described_class.new(params)
|
211
|
+
driver.monkey_test(Xcmonkey.new(params).gestures)
|
212
|
+
expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'verifies that repeat_monkey_test works fine' do
|
216
|
+
session_actions = [
|
217
|
+
{ 'type' => 'tap', 'x' => 10, 'y' => 10 },
|
218
|
+
{ 'type' => 'press', 'x' => 11, 'y' => 11, 'duration' => 1.4 },
|
219
|
+
{ 'type' => 'swipe', 'x' => 12, 'y' => 12, 'endX' => 15, 'endY' => 15, 'duration' => 0.3 }
|
220
|
+
]
|
221
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id, session_actions: session_actions)
|
222
|
+
expect(driver).to receive(:tap).with(coordinates: { x: 10, y: 10 })
|
223
|
+
expect(driver).to receive(:press).with(coordinates: { x: 11, y: 11 }, duration: 1.4)
|
224
|
+
expect(driver).to receive(:swipe).with(start_coordinates: { x: 12, y: 12 }, end_coordinates: { x: 15, y: 15 }, duration: 0.3)
|
225
|
+
driver.repeat_monkey_test
|
226
|
+
expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'verifies that unknown actions does not break repeat_monkey_test' do
|
230
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ 'type' => 'test', 'x' => 10, 'y' => 10 }])
|
231
|
+
expect(driver).to receive(:monkey_test_precondition)
|
232
|
+
expect(driver).not_to receive(:tap)
|
233
|
+
expect(driver).not_to receive(:press)
|
234
|
+
expect(driver).not_to receive(:swipe)
|
235
|
+
driver.repeat_monkey_test
|
236
|
+
expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'verifies that running apps are tracked' do
|
240
|
+
new_app_bundle_id = 'com.apple.Preferences'
|
241
|
+
driver.terminate_app(new_app_bundle_id)
|
242
|
+
driver.monkey_test_precondition
|
243
|
+
driver.launch_app(target_bundle_id: new_app_bundle_id, wait_for_state_update: true)
|
244
|
+
expect(driver).to receive(:launch_app).with(target_bundle_id: bundle_id)
|
245
|
+
expect(driver).to receive(:terminate_app).with(new_app_bundle_id)
|
246
|
+
driver.track_running_apps
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'verifies that running apps can be determined' do
|
250
|
+
driver.terminate_app(bundle_id)
|
251
|
+
sum = driver.list_running_apps.size
|
252
|
+
driver.launch_app(target_bundle_id: bundle_id)
|
253
|
+
expect(driver.list_running_apps.size).to eq(sum + 1)
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'verifies that app state change can be determined' do
|
257
|
+
driver.launch_app(target_bundle_id: bundle_id)
|
258
|
+
allow(driver).to receive(:detect_app_in_background).and_return(true)
|
259
|
+
expect(driver).not_to receive(:save_session)
|
260
|
+
expect(driver).to receive(:launch_app)
|
261
|
+
expect { driver.detect_app_state_change }.not_to raise_error
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'verifies that background is the invalid app state' do
|
265
|
+
driver.terminate_app(bundle_id)
|
266
|
+
expect(driver).to receive(:save_session)
|
267
|
+
expect { driver.detect_app_state_change }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'verifies that foreground is the valid app state' do
|
271
|
+
driver.launch_app(target_bundle_id: bundle_id, wait_for_state_update: true)
|
272
|
+
expect { driver.detect_app_state_change }.not_to raise_error
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'verifies that background state can be determined' do
|
276
|
+
driver.terminate_app(bundle_id)
|
277
|
+
expect(driver.detect_app_in_background).to be(true)
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'verifies that foregroung state can be determined' do
|
281
|
+
driver.monkey_test_precondition
|
282
|
+
expect(driver.detect_app_in_background).to be(false)
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'verifies that xcmonkey behaves as expected on real devices' do
|
286
|
+
udid = '1234-5678'
|
287
|
+
driver = described_class.new(udid: udid, bundle_id: bundle_id)
|
288
|
+
allow(driver).to receive(:list_targets).and_return([{ 'udid' => udid, 'type' => 'device' }])
|
289
|
+
expect { driver.ensure_device_exists }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
|
290
|
+
end
|
291
|
+
|
214
292
|
it 'verifies that simulator was not booted' do
|
215
293
|
driver.shutdown_simulator
|
216
294
|
error_message = "Failed to boot #{udid}"
|
data/spec/repeater_spec.rb
CHANGED
@@ -7,7 +7,6 @@ describe Repeater do
|
|
7
7
|
let(:session_file_content_without_bundle_id) { '{ "params": {"udid": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
8
8
|
let(:session_file_content_without_udid) { '{ "params": {"bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
|
9
9
|
|
10
|
-
# TESTME
|
11
10
|
it 'verifies that session cannot be validated without params' do
|
12
11
|
allow(File).to receive(:exist?).and_return(true)
|
13
12
|
allow(File).to receive(:read).and_return(session_file_content_without_params)
|
data/spec/xcmonkey_spec.rb
CHANGED
@@ -1,58 +1,56 @@
|
|
1
1
|
describe Xcmonkey do
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
expect(Xcmonkey::GEM_NAME).to eq('xcmonkey')
|
56
|
-
end
|
2
|
+
let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10, session_path: Dir.pwd } }
|
3
|
+
let(:duration_error_msg) { 'Duration must be Integer and not less than 1 second' }
|
4
|
+
|
5
|
+
before do
|
6
|
+
allow(Logger).to receive(:info)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'verifies gestures' do
|
10
|
+
gestures = described_class.new(params).gestures
|
11
|
+
taps = [:precise_tap, :blind_tap] * 10
|
12
|
+
swipes = [:precise_swipe, :blind_swipe] * 5
|
13
|
+
presses = [:precise_press, :blind_press]
|
14
|
+
expect(gestures) =~ presses + taps + swipes
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'verifies required params' do
|
18
|
+
expect(Logger).not_to receive(:error)
|
19
|
+
described_class.new(params)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'verifies `udid` param is required' do
|
23
|
+
params[:udid] = nil
|
24
|
+
expect(Logger).to receive(:error).with('UDID should be provided')
|
25
|
+
described_class.new(params)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'verifies `bundle_id` param is required' do
|
29
|
+
params[:bundle_id] = nil
|
30
|
+
expect(Logger).to receive(:error).with('Bundle identifier should be provided')
|
31
|
+
described_class.new(params)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'verifies `duration` param is optional' do
|
35
|
+
params[:duration] = nil
|
36
|
+
expect(Logger).not_to receive(:error)
|
37
|
+
described_class.new(params)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'verifies `duration` param cannot be equal to zero' do
|
41
|
+
params[:duration] = 0
|
42
|
+
expect(Logger).to receive(:error).with(duration_error_msg)
|
43
|
+
described_class.new(params)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'verifies `duration` param cannot be negative' do
|
47
|
+
params[:duration] = -1
|
48
|
+
expect(Logger).to receive(:error).with(duration_error_msg)
|
49
|
+
described_class.new(params)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'verifies version' do
|
53
|
+
current_version = Gem::Version.new(Xcmonkey::VERSION)
|
54
|
+
expect(current_version).to be > Gem::Version.new('0.1.0')
|
57
55
|
end
|
58
56
|
end
|
data/xcmonkey.gemspec
CHANGED
@@ -3,7 +3,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
3
|
require "xcmonkey/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name =
|
6
|
+
spec.name = "xcmonkey"
|
7
7
|
spec.version = Xcmonkey::VERSION
|
8
8
|
spec.authors = ["alteral"]
|
9
9
|
spec.email = ["a.alterpesotskiy@mail.ru"]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xcmonkey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- alteral
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-01-
|
11
|
+
date: 2023-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -216,9 +216,9 @@ extra_rdoc_files: []
|
|
216
216
|
files:
|
217
217
|
- ".fasterer.yml"
|
218
218
|
- ".github/FUNDING.yml"
|
219
|
+
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
220
|
+
- ".github/ISSUE_TEMPLATE/feature-request.md"
|
219
221
|
- ".github/dependabot.yml"
|
220
|
-
- ".github/issue_template/bug_report.md"
|
221
|
-
- ".github/issue_template/feature_request.md"
|
222
222
|
- ".github/pull_request_template.md"
|
223
223
|
- ".github/workflows/test.yml"
|
224
224
|
- ".gitignore"
|