xcmonkey 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +0 -1
- data/.github/workflows/test.yml +1 -1
- data/README.md +31 -21
- data/assets/images/xcmonkey.png +0 -0
- data/lib/xcmonkey/driver.rb +63 -18
- data/lib/xcmonkey/version.rb +1 -1
- data/lib/xcmonkey.rb +5 -2
- data/spec/driver_spec.rb +66 -4
- data/spec/logger_spec.rb +20 -0
- data/spec/xcmonkey_spec.rb +13 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be6683e9fefe9666fb294e9939f5ebd83c71915307002dc6425413dfd1d9ae8b
|
4
|
+
data.tar.gz: d4782c2269b36dd950b7567c88d554c7b000cc81452f79e6659d4061135c1ebe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d027d46289c6ea5bfba7359ff5c0b8172ea7d12d5d9cd1019a428b7b74416e6562930db5ce14508198937caed55ac2b5f1c68f1ce34c82fbe6ecbf3a92ea7100
|
7
|
+
data.tar.gz: 6d336b581f1bb93db017414b7080e038557200909e8a87b3a23c37fce4dfdd62b9fc5186c45272e662002f073c00a800f172488495b9bf84cd91d0a9089e275c
|
data/.github/FUNDING.yml
CHANGED
data/.github/workflows/test.yml
CHANGED
data/README.md
CHANGED
@@ -9,8 +9,14 @@
|
|
9
9
|
<a href="/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg?style=flat" /></a>
|
10
10
|
</p>
|
11
11
|
|
12
|
+
## Description
|
13
|
+
|
12
14
|
*xcmonkey* is a tool for doing randomised UI testing of iOS apps.
|
13
15
|
|
16
|
+
It is inspired by and has similar goals to Android [monkey](https://developer.android.com/studio/test/monkey).
|
17
|
+
|
18
|
+
*xcmonkey* uses [idb](https://fbidb.io) as a driver that's why it's quite smart and can do a lot of things, such as taps, swipes and presses. Because *xcmonkey* has access to the screen hierarchy, it can either do things blindly (like tapping on random points) or precisely (like tapping on the existing elements).
|
19
|
+
|
14
20
|
## Requirements
|
15
21
|
|
16
22
|
```bash
|
@@ -24,22 +30,28 @@ pip3.6 install fb-idb
|
|
24
30
|
gem install xcmonkey
|
25
31
|
```
|
26
32
|
|
33
|
+
If you prefer to use [bundler](https://bundler.io/), add the following line to your `Gemfile`:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gem 'xcmonkey'
|
37
|
+
```
|
38
|
+
|
27
39
|
## Usage
|
28
40
|
|
29
|
-
###
|
41
|
+
### To run a stress test
|
30
42
|
|
31
43
|
```bash
|
32
|
-
$ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.
|
44
|
+
$ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.Maps" --duration 100
|
33
45
|
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
|
34
46
|
|
35
|
-
12:44:22.550: App info: com.apple.
|
47
|
+
12:44:22.550: App info: com.apple.Maps | Maps | system | arm64, x86_64 | Running | Not Debuggable | pid=74636
|
36
48
|
|
37
49
|
12:44:23.203: Tap: {
|
38
50
|
"x": 53,
|
39
51
|
"y": 749
|
40
52
|
}
|
41
53
|
|
42
|
-
12:44:23.511: Swipe: {
|
54
|
+
12:44:23.511: Swipe (0.5s): {
|
43
55
|
"x": 196,
|
44
56
|
"y": 426
|
45
57
|
} => {
|
@@ -47,39 +59,37 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
|
|
47
59
|
"y": 447
|
48
60
|
}
|
49
61
|
|
50
|
-
12:44:24.355:
|
62
|
+
12:44:24.355: Press (1.2s): {
|
51
63
|
"x": 143,
|
52
64
|
"y": 323
|
53
65
|
}
|
54
66
|
```
|
55
67
|
|
56
|
-
###
|
68
|
+
### To describe the required point
|
57
69
|
|
58
70
|
```bash
|
59
|
-
$ xcmonkey describe -x
|
60
|
-
|
71
|
+
$ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
|
72
|
+
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
|
61
73
|
|
62
|
-
|
63
|
-
"AXFrame": "{{
|
64
|
-
"AXUniqueId": "
|
74
|
+
20:05:21.713: x:20 y:625 point info: {
|
75
|
+
"AXFrame": "{{19, 624.33333333333337}, {86, 130.66666666666663}}",
|
76
|
+
"AXUniqueId": "ShortcutsRowCell",
|
65
77
|
"frame": {
|
66
|
-
"y":
|
67
|
-
"x":
|
68
|
-
"width":
|
69
|
-
"height":
|
78
|
+
"y": 624.3333333333334,
|
79
|
+
"x": 19,
|
80
|
+
"width": 86,
|
81
|
+
"height": 130.66666666666663
|
70
82
|
},
|
71
83
|
"role_description": "button",
|
72
|
-
"AXLabel": "
|
84
|
+
"AXLabel": "Home",
|
73
85
|
"content_required": false,
|
74
86
|
"type": "Button",
|
75
87
|
"title": null,
|
76
|
-
"help":
|
88
|
+
"help": null,
|
77
89
|
"custom_actions": [
|
78
|
-
|
79
|
-
"Today",
|
80
|
-
"App Library"
|
90
|
+
|
81
91
|
],
|
82
|
-
"AXValue": "",
|
92
|
+
"AXValue": "Add",
|
83
93
|
"enabled": true,
|
84
94
|
"role": "AXButton",
|
85
95
|
"subrole": null
|
data/assets/images/xcmonkey.png
CHANGED
Binary file
|
data/lib/xcmonkey/driver.rb
CHANGED
@@ -18,11 +18,23 @@ class Driver
|
|
18
18
|
when :precise_tap
|
19
19
|
tap(coordinates: el1_coordinates)
|
20
20
|
when :blind_tap
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
when :
|
25
|
-
|
21
|
+
tap(coordinates: random_coordinates)
|
22
|
+
when :precise_press
|
23
|
+
press(coordinates: el1_coordinates, duration: press_duration)
|
24
|
+
when :blind_press
|
25
|
+
press(coordinates: random_coordinates, duration: press_duration)
|
26
|
+
when :precise_swipe
|
27
|
+
swipe(
|
28
|
+
start_coordinates: el1_coordinates,
|
29
|
+
end_coordinates: el2_coordinates,
|
30
|
+
duration: swipe_duration
|
31
|
+
)
|
32
|
+
when :blind_swipe
|
33
|
+
swipe(
|
34
|
+
start_coordinates: random_coordinates,
|
35
|
+
end_coordinates: random_coordinates,
|
36
|
+
duration: swipe_duration
|
37
|
+
)
|
26
38
|
else
|
27
39
|
next
|
28
40
|
end
|
@@ -31,9 +43,9 @@ class Driver
|
|
31
43
|
end
|
32
44
|
end
|
33
45
|
|
34
|
-
def open_home_screen(
|
46
|
+
def open_home_screen(with_tracker: false)
|
35
47
|
`idb ui button --udid #{udid} HOME`
|
36
|
-
detect_home_unique_element if
|
48
|
+
detect_home_unique_element if with_tracker
|
37
49
|
end
|
38
50
|
|
39
51
|
def describe_ui
|
@@ -57,7 +69,7 @@ class Driver
|
|
57
69
|
|
58
70
|
def boot_simulator
|
59
71
|
`idb boot #{udid}`
|
60
|
-
|
72
|
+
Logger.error("Failed to boot #{udid}") if device_info['state'] != 'Booted'
|
61
73
|
end
|
62
74
|
|
63
75
|
def shutdown_simulator
|
@@ -84,11 +96,6 @@ class Driver
|
|
84
96
|
boot_simulator if device.include?('simulator')
|
85
97
|
end
|
86
98
|
|
87
|
-
def ensure_simulator_was_booted
|
88
|
-
sim = list_booted_simulators.detect { |target| target.include?(udid) }
|
89
|
-
Logger.error("Failed to boot #{udid}") if sim.nil?
|
90
|
-
end
|
91
|
-
|
92
99
|
def list_apps
|
93
100
|
`idb list-apps --udid #{udid}`
|
94
101
|
end
|
@@ -98,20 +105,58 @@ class Driver
|
|
98
105
|
`idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
|
99
106
|
end
|
100
107
|
|
101
|
-
def
|
102
|
-
Logger.info(
|
108
|
+
def press(coordinates:, duration:)
|
109
|
+
Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
110
|
+
`idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
|
111
|
+
end
|
112
|
+
|
113
|
+
def swipe(start_coordinates:, end_coordinates:, duration:)
|
114
|
+
Logger.info(
|
115
|
+
"Swipe (#{duration}s):",
|
116
|
+
payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
|
117
|
+
)
|
103
118
|
coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
|
104
|
-
`idb ui swipe --udid #{udid} --duration
|
119
|
+
`idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
|
105
120
|
end
|
106
121
|
|
107
122
|
def central_coordinates(element)
|
108
123
|
frame = element['frame']
|
124
|
+
x = (frame['x'] + (frame['width'] / 2)).abs.to_i
|
125
|
+
y = (frame['y'] + (frame['height'] / 2)).abs.to_i
|
126
|
+
{
|
127
|
+
x: x > screen_size[:width].to_i ? rand(0..screen_size[:width].to_i) : x,
|
128
|
+
y: y > screen_size[:height].to_i ? rand(0..screen_size[:height].to_i) : y
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
def random_coordinates
|
133
|
+
{
|
134
|
+
x: rand(0..screen_size[:width].to_i),
|
135
|
+
y: rand(0..screen_size[:height].to_i)
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
def device_info
|
140
|
+
@device_info ||= JSON.parse(`idb describe --udid #{udid} --json`)
|
141
|
+
@device_info
|
142
|
+
end
|
143
|
+
|
144
|
+
def screen_size
|
145
|
+
screen_dimensions = device_info['screen_dimensions']
|
109
146
|
{
|
110
|
-
|
111
|
-
|
147
|
+
width: screen_dimensions['width_points'],
|
148
|
+
height: screen_dimensions['height_points']
|
112
149
|
}
|
113
150
|
end
|
114
151
|
|
152
|
+
def swipe_duration
|
153
|
+
rand(0.1..0.7).ceil(1)
|
154
|
+
end
|
155
|
+
|
156
|
+
def press_duration
|
157
|
+
rand(0.5..1.5).ceil(1)
|
158
|
+
end
|
159
|
+
|
115
160
|
private
|
116
161
|
|
117
162
|
def ensure_driver_installed
|
data/lib/xcmonkey/version.rb
CHANGED
data/lib/xcmonkey.rb
CHANGED
@@ -21,13 +21,16 @@ module Xcmonkey
|
|
21
21
|
driver.ensure_device_exists
|
22
22
|
driver.ensure_app_installed
|
23
23
|
driver.terminate_app
|
24
|
-
driver.open_home_screen(
|
24
|
+
driver.open_home_screen(with_tracker: true)
|
25
25
|
driver.launch_app
|
26
26
|
driver.monkey_test(gestures)
|
27
27
|
end
|
28
28
|
|
29
29
|
def gestures
|
30
|
-
[:precise_tap, :blind_tap
|
30
|
+
taps = [:precise_tap, :blind_tap] * 10
|
31
|
+
swipes = [:precise_swipe, :blind_swipe] * 5
|
32
|
+
presses = [:precise_press, :blind_press]
|
33
|
+
taps + swipes + presses
|
31
34
|
end
|
32
35
|
|
33
36
|
def ensure_required_params(params)
|
data/spec/driver_spec.rb
CHANGED
@@ -6,8 +6,7 @@ describe Driver do
|
|
6
6
|
it 'verifies that sumulator was booted' do
|
7
7
|
error_message = "Failed to boot #{udid}"
|
8
8
|
expect(Logger).not_to receive(:error).with(error_message, payload: nil)
|
9
|
-
expect
|
10
|
-
driver.boot_simulator
|
9
|
+
expect { driver.boot_simulator }.not_to raise_error
|
11
10
|
end
|
12
11
|
|
13
12
|
it 'verifies that there are booted simulators' do
|
@@ -24,10 +23,16 @@ describe Driver do
|
|
24
23
|
|
25
24
|
it 'verifies that home screen can be opened' do
|
26
25
|
driver.boot_simulator
|
27
|
-
home_tracker = driver.open_home_screen(
|
26
|
+
home_tracker = driver.open_home_screen(with_tracker: true)
|
28
27
|
expect(home_tracker).not_to be_empty
|
29
28
|
end
|
30
29
|
|
30
|
+
it 'verifies that home screen can be opened without tracker' do
|
31
|
+
driver.boot_simulator
|
32
|
+
home_tracker = driver.open_home_screen(with_tracker: false)
|
33
|
+
expect(home_tracker).to be_nil
|
34
|
+
end
|
35
|
+
|
31
36
|
it 'verifies that list of targets can be showed' do
|
32
37
|
list_targets = driver.list_targets
|
33
38
|
expect(list_targets).not_to be_empty
|
@@ -85,6 +90,38 @@ describe Driver do
|
|
85
90
|
expect(actual_coordinates).to eq(expected_coordinates)
|
86
91
|
end
|
87
92
|
|
93
|
+
it 'verifies that device info can be for booted simulator' do
|
94
|
+
driver.boot_simulator
|
95
|
+
expect(driver.device_info).not_to be_empty
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'verifies that device info can be for not booted simulator' do
|
99
|
+
driver.shutdown_simulator
|
100
|
+
expect(driver.device_info).not_to be_empty
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'verifies that screen size can be found' do
|
104
|
+
driver.boot_simulator
|
105
|
+
screen_size = driver.screen_size
|
106
|
+
expect(screen_size[:width]).to be > 0
|
107
|
+
expect(screen_size[:height]).to be > 0
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'verifies that random coordinates can be found' do
|
111
|
+
driver.boot_simulator
|
112
|
+
coordinates = driver.random_coordinates
|
113
|
+
expect(coordinates[:x]).to be > 0
|
114
|
+
expect(coordinates[:y]).to be > 0
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'verifies swipe duration' do
|
118
|
+
expect(driver.swipe_duration).to be_between(0.1, 0.7)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'verifies press duration' do
|
122
|
+
expect(driver.press_duration).to be_between(0.5, 1.5)
|
123
|
+
end
|
124
|
+
|
88
125
|
it 'verifies that app can be launched' do
|
89
126
|
expect(Logger).not_to receive(:error)
|
90
127
|
expect(Logger).to receive(:info)
|
@@ -93,10 +130,35 @@ describe Driver do
|
|
93
130
|
expect { driver.launch_app }.not_to raise_error
|
94
131
|
end
|
95
132
|
|
133
|
+
it 'verifies tap' do
|
134
|
+
driver.boot_simulator
|
135
|
+
coordinates = { x: 1, y: 1 }
|
136
|
+
expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
|
137
|
+
driver.tap(coordinates: coordinates)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'verifies press' do
|
141
|
+
driver.boot_simulator
|
142
|
+
duration = 0.5
|
143
|
+
coordinates = { x: 1, y: 1 }
|
144
|
+
expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
|
145
|
+
driver.press(coordinates: coordinates, duration: duration)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'verifies swipe' do
|
149
|
+
driver.boot_simulator
|
150
|
+
duration = 0.5
|
151
|
+
start_coordinates = { x: 1, y: 1 }
|
152
|
+
end_coordinates = { x: 2, y: 2 }
|
153
|
+
expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
|
154
|
+
driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
|
155
|
+
end
|
156
|
+
|
96
157
|
it 'verifies that simulator was not booted' do
|
97
158
|
driver.shutdown_simulator
|
98
159
|
error_message = "Failed to boot #{udid}"
|
160
|
+
allow(driver).to receive(:device_info).and_return({ 'state' => 'Unknown' })
|
99
161
|
expect(Logger).to receive(:log).with(error_message, color: :light_red, payload: nil)
|
100
|
-
expect { driver.
|
162
|
+
expect { driver.boot_simulator }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
|
101
163
|
end
|
102
164
|
end
|
data/spec/logger_spec.rb
CHANGED
@@ -33,4 +33,24 @@ describe Logger do
|
|
33
33
|
expect { described_class.error(message, payload: payload) }
|
34
34
|
.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
|
35
35
|
end
|
36
|
+
|
37
|
+
it 'verifies custom log without payload' do
|
38
|
+
color = :blue
|
39
|
+
time = Time.now
|
40
|
+
allow(Time).to receive(:now).and_return(time)
|
41
|
+
expected_output = "#{time.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
|
42
|
+
expect do
|
43
|
+
described_class.log(message, color: color, payload: nil)
|
44
|
+
end.to output("#{expected_output}\n\n").to_stdout
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'verifies custom log with payload' do
|
48
|
+
color = :blue
|
49
|
+
time = Time.now
|
50
|
+
allow(Time).to receive(:now).and_return(time)
|
51
|
+
expected_output = "#{time.strftime('%k:%M:%S.%L')}: #{message}".colorize(color)
|
52
|
+
expect do
|
53
|
+
described_class.log(message, color: color, payload: payload)
|
54
|
+
end.to output("#{expected_output} #{payload.colorize(:light_green)}\n\n").to_stdout
|
55
|
+
end
|
36
56
|
end
|
data/spec/xcmonkey_spec.rb
CHANGED
@@ -5,7 +5,10 @@ describe Xcmonkey do
|
|
5
5
|
|
6
6
|
it 'verifies gestures' do
|
7
7
|
gestures = described_class.new(params).gestures
|
8
|
-
|
8
|
+
taps = [:precise_tap, :blind_tap] * 10
|
9
|
+
swipes = [:precise_swipe, :blind_swipe] * 5
|
10
|
+
presses = [:precise_press, :blind_press]
|
11
|
+
expect(gestures) =~ presses + taps + swipes
|
9
12
|
end
|
10
13
|
|
11
14
|
it 'verifies required params' do
|
@@ -42,5 +45,14 @@ describe Xcmonkey do
|
|
42
45
|
expect(Logger).to receive(:error).with(duration_error_msg)
|
43
46
|
described_class.new(params)
|
44
47
|
end
|
48
|
+
|
49
|
+
it 'verifies version' do
|
50
|
+
current_version = Gem::Version.new(Xcmonkey::VERSION)
|
51
|
+
expect(current_version).to be > Gem::Version.new('0.1.0')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'verifies gem name' do
|
55
|
+
expect(Xcmonkey::GEM_NAME).to eq('xcmonkey')
|
56
|
+
end
|
45
57
|
end
|
46
58
|
end
|
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: 0.
|
4
|
+
version: 0.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: 2022-12-
|
11
|
+
date: 2022-12-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|