xcmonkey 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a6aa190c1c4cfa28f97b20524c907da488aad2d78f2c1dda8f827899f6ee53f
4
- data.tar.gz: 534245a695ed0f030df459986c326fb356b3593baec3bc6750eb1cc622d32390
3
+ metadata.gz: be6683e9fefe9666fb294e9939f5ebd83c71915307002dc6425413dfd1d9ae8b
4
+ data.tar.gz: d4782c2269b36dd950b7567c88d554c7b000cc81452f79e6659d4061135c1ebe
5
5
  SHA512:
6
- metadata.gz: 41415c8c576a3b6887ee87d44f4c4e571f1e7abda3a96f23abff52cebe77438be70ee76ab18f83a92b78cca2da429cf0c12305b2d3a8ca59b0b2e7e26405ec23
7
- data.tar.gz: 93d86451b2b7b8d2a755744ea47be375d042763ea550ddbdef5faff530bbf775e46ab24ede0f6d9dfe424f02ef4c2a923ccd3f342d1872cd6f4d82dcefae7cef
6
+ metadata.gz: d027d46289c6ea5bfba7359ff5c0b8172ea7d12d5d9cd1019a428b7b74416e6562930db5ce14508198937caed55ac2b5f1c68f1ce34c82fbe6ecbf3a92ea7100
7
+ data.tar.gz: 6d336b581f1bb93db017414b7080e038557200909e8a87b3a23c37fce4dfdd62b9fc5186c45272e662002f073c00a800f172488495b9bf84cd91d0a9089e275c
data/.github/FUNDING.yml CHANGED
@@ -1,3 +1,2 @@
1
1
  custom: [ 'https://revolut.me/alteral', 'https://paypal.me/aapesotskiy' ]
2
- github: alteral
3
2
  ko_fi: alteral
@@ -13,7 +13,7 @@ jobs:
13
13
  chat:
14
14
  name: Automated Code Review
15
15
  runs-on: macos-12
16
- timeout-minutes: 15
16
+ timeout-minutes: 30
17
17
  env:
18
18
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19
19
  SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
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
- ### Test
41
+ ### To run a stress test
30
42
 
31
43
  ```bash
32
- $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.apple.mobilesafari"
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.mobilesafari | MobileSafari | system | x86_64, arm64 | Running | Not Debuggable | pid=43398
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: Tap: {
62
+ 12:44:24.355: Press (1.2s): {
51
63
  "x": 143,
52
64
  "y": 323
53
65
  }
54
66
  ```
55
67
 
56
- ### Describe point
68
+ ### To describe the required point
57
69
 
58
70
  ```bash
59
- $ xcmonkey describe -x 125 -y 760 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
60
- 12:41:03.840: 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
+ $ 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
- 12:41:05.342: x:125 y:760 point info: {
63
- "AXFrame": "{{120, 759}, {64, 64}}",
64
- "AXUniqueId": "Safari",
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": 759,
67
- "x": 120,
68
- "width": 64,
69
- "height": 64
78
+ "y": 624.3333333333334,
79
+ "x": 19,
80
+ "width": 86,
81
+ "height": 130.66666666666663
70
82
  },
71
83
  "role_description": "button",
72
- "AXLabel": "Safari",
84
+ "AXLabel": "Home",
73
85
  "content_required": false,
74
86
  "type": "Button",
75
87
  "title": null,
76
- "help": "Double tap to open",
88
+ "help": null,
77
89
  "custom_actions": [
78
- "Edit mode",
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
Binary file
@@ -18,11 +18,23 @@ class Driver
18
18
  when :precise_tap
19
19
  tap(coordinates: el1_coordinates)
20
20
  when :blind_tap
21
- x = (el1_coordinates[:x] - el2_coordinates[:x]).abs
22
- y = (el1_coordinates[:y] - el2_coordinates[:y]).abs
23
- tap(coordinates: { x: x, y: y })
24
- when :swipe
25
- swipe(start_coordinates: el1_coordinates, end_coordinates: el2_coordinates)
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(return_tracker: false)
46
+ def open_home_screen(with_tracker: false)
35
47
  `idb ui button --udid #{udid} HOME`
36
- detect_home_unique_element if return_tracker
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
- ensure_simulator_was_booted
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 swipe(start_coordinates:, end_coordinates:)
102
- Logger.info('Swipe:', payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
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 0.5 #{coordinates}`
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
- x: (frame['x'] + (frame['width'] / 2)).to_i,
111
- y: (frame['y'] + (frame['height'] / 2)).to_i
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
@@ -1,4 +1,4 @@
1
1
  module Xcmonkey
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  GEM_NAME = 'xcmonkey'
4
4
  end
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(return_tracker: true)
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, :swipe]
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(driver).to receive(:ensure_simulator_was_booted)
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(return_tracker: true)
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.ensure_simulator_was_booted }.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
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
@@ -5,7 +5,10 @@ describe Xcmonkey do
5
5
 
6
6
  it 'verifies gestures' do
7
7
  gestures = described_class.new(params).gestures
8
- expect(gestures) =~ [:swipe, :precise_tap, :blind_tap]
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.1.2
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-28 00:00:00.000000000 Z
11
+ date: 2022-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler