xcmonkey 0.3.0 → 1.1.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: 48bd9b79b2370e4f20366973e689af344f6c75729b361658fbc5bb2c02c38253
4
- data.tar.gz: c207e8dbe7c820623f60a9c0b9e8ea8a7e5bce18bfe10f9d5c27e8928829889d
3
+ metadata.gz: 7d3bd605b57ec05a83a3cca39707b5c2fc9529828131b9ac872f3dc478180bbb
4
+ data.tar.gz: 8793e64f73775a2c8de84d1b79d0aebd258df7c22b9c8f43de2618f63f306d4e
5
5
  SHA512:
6
- metadata.gz: 9c0f1f52ad5e8118799556e4e1ba3a49574c504cf2a840fe0369e06c6dbab260fd0a9c275ada9efb789a865759ac48f7d46f9348cde5b2cb53a44969b5189cf9
7
- data.tar.gz: 761ffee247c8646bc24dee5fd3f473414402033d3c9a773fe87623579aef93155ecbafc3d412111396945ae485d74b28d6916e5e287754a82b15fcfc792b0c47
6
+ metadata.gz: 4e4634518e71dac0ed206d4fec26fc707fb9dbea8116c87db1959695261e8ee311246db203317790433d18b5e5250074f1f17bf567101dfec6767a1ed5db0670
7
+ data.tar.gz: 0e696e9804b94e6106f203b9bcda008f57dbe7fbda78a68ff7d57a8cfa980e4b691b533f430bac855ae8ed56ae0e7ac5085f305853bae8f03652095871287a94
@@ -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
@@ -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
@@ -1,9 +1,14 @@
1
1
  # Changes
2
2
 
3
3
  ## References
4
+
4
5
  - https://github.com/alteral/xcmonkey/issues/XXX
5
6
 
7
+ ## Description
8
+
9
+
6
10
  ## Risks
11
+
7
12
  - [ ] None
8
13
  - [ ] Low
9
14
  - [ ] High
@@ -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.2.0
22
+ - uses: actions/checkout@v3.3.0
22
23
 
23
- - uses: actions/setup-python@v4.4.0
24
+ - uses: actions/setup-python@v4.5.0
24
25
  with:
25
26
  python-version: 3.11
26
27
  cache: 'pip'
data/.gitignore CHANGED
@@ -39,3 +39,6 @@ DerivedData
39
39
 
40
40
  # Sonar
41
41
  .scannerwork
42
+
43
+ # Cache
44
+ xcmonkey-session.json
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## Description
13
13
 
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.
15
15
 
16
16
  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).
17
17
 
@@ -39,10 +39,32 @@ gem 'xcmonkey'
39
39
  ### To run a stress test
40
40
 
41
41
  ```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
+ }
44
55
 
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
+ }
46
68
 
47
69
  12:44:23.203: Tap: {
48
70
  "x": 53,
@@ -63,35 +85,32 @@ $ xcmonkey test --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA" --bundle-id "com.a
63
85
  }
64
86
  ```
65
87
 
88
+ ### To repeat the stress test from generated session
89
+
90
+ ```bash
91
+ xcmonkey repeat --session-path "./xcmonkey-session.json"
92
+ ```
93
+
66
94
  ### To describe the required point
67
95
 
68
96
  ```bash
69
- $ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
70
- 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
71
-
72
- 20:05:21.713: x:20 y:625 point info: {
73
- "AXFrame": "{{19, 624.3}, {86, 130.6}}",
74
- "AXUniqueId": "ShortcutsRowCell",
75
- "frame": {
76
- "y": 624.3,
77
- "x": 19,
78
- "width": 86,
79
- "height": 130.6
80
- },
81
- "role_description": "button",
82
- "AXLabel": "Home",
83
- "content_required": false,
84
- "type": "Button",
85
- "title": null,
86
- "help": null,
87
- "custom_actions": [
97
+ xcmonkey describe -x 20 -y 625 --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
98
+ ```
88
99
 
89
- ],
90
- "AXValue": "Add",
91
- "enabled": true,
92
- "role": "AXButton",
93
- "subrole": null
94
- }
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
95
114
  ```
96
115
 
97
116
  ## Code of Conduct
data/bin/xcmonkey CHANGED
@@ -3,11 +3,12 @@
3
3
  require 'commander/import'
4
4
  require_relative '../lib/xcmonkey'
5
5
  require_relative '../lib/xcmonkey/describer'
6
+ require_relative '../lib/xcmonkey/repeater'
6
7
  require_relative '../lib/xcmonkey/logger'
7
8
  require_relative '../lib/xcmonkey/driver'
8
9
  require_relative '../lib/xcmonkey/version'
9
10
 
10
- module Xcmonkey
11
+ class Xcmonkey
11
12
  program :version, VERSION
12
13
  program :description, 'xcmonkey is a tool for doing randomised UI testing of iOS apps'
13
14
 
@@ -18,25 +19,36 @@ module Xcmonkey
18
19
  c.option('-b', '--bundle-id STRING', String, 'Set target bundle identifier')
19
20
  c.option('-d', '--duration SECONDS', Integer, 'Test duration in seconds. Defaults to `60`')
20
21
  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|
23
24
  params = {
24
25
  udid: options.udid,
25
26
  bundle_id: options.bundle_id,
26
27
  duration: options.duration,
27
- simulator_keyboard: options.enable_simulator_keyboard
28
+ session_path: options.session_path,
29
+ enable_simulator_keyboard: options.enable_simulator_keyboard
28
30
  }
29
31
  Xcmonkey.new(params).run
30
32
  end
31
33
  end
32
34
 
35
+ command :repeat do |c|
36
+ c.syntax = 'xcmonkey repeat [options]'
37
+ c.description = 'Repeats given session'
38
+ c.option('-s', '--session-path STRING', String, 'Path to monkey testing session')
39
+ c.action do |_, options|
40
+ params = { session_path: options.session_path }
41
+ Repeater.new(params).run
42
+ end
43
+ end
44
+
33
45
  command :describe do |c|
34
46
  c.syntax = 'xcmonkey describe [options]'
35
47
  c.description = 'Describes given point'
36
48
  c.option('-u', '--udid STRING', String, 'Set device UDID')
37
49
  c.option('-x', '--x STRING', 'Point `x` coordinate')
38
50
  c.option('-y', '--y STRING', 'Point `y` coordinate')
39
- c.action do |args, options|
51
+ c.action do |_, options|
40
52
  params = {
41
53
  udid: options.udid,
42
54
  x: options.x,
@@ -1,22 +1,21 @@
1
1
  class Describer
2
- attr_accessor :udid, :x, :y, :driver
2
+ attr_accessor :x, :y, :driver
3
3
 
4
- def initialize(params)
5
- ensure_required_params(params)
6
- self.udid = params[:udid]
7
- self.x = params[:x]
8
- self.y = params[:y]
9
- self.driver = Driver.new(params)
10
- 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
11
10
 
12
- def run
13
- driver.ensure_device_exists
14
- driver.describe_point(x, y)
15
- end
11
+ def run
12
+ driver.ensure_device_exists
13
+ driver.describe_point(x, y)
14
+ end
16
15
 
17
- def ensure_required_params(params)
18
- Logger.error('UDID should be provided') if params[:udid].nil?
19
- Logger.error('`x` point coordinate should be provided') if params[:x].nil? || params[:x].to_i.to_s != params[:x].to_s
20
- Logger.error('`y` point coordinate should be provided') if params[:y].nil? || params[:y].to_i.to_s != params[:y].to_s
21
- 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
22
21
  end
@@ -1,18 +1,31 @@
1
1
  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
3
3
 
4
4
  def initialize(params)
5
5
  self.udid = params[:udid]
6
6
  self.bundle_id = params[:bundle_id]
7
- self.duration = params[:duration]
7
+ self.session_duration = params[:duration]
8
+ self.session_path = params[:session_path]
8
9
  self.enable_simulator_keyboard = params[:enable_simulator_keyboard]
10
+ self.session_actions = params[:session_actions]
11
+ @session = { params: params, actions: [] }
9
12
  ensure_driver_installed
10
13
  end
11
14
 
15
+ def monkey_test_precondition
16
+ puts
17
+ ensure_device_exists
18
+ ensure_app_installed
19
+ terminate_app
20
+ open_home_screen(with_tracker: true)
21
+ launch_app
22
+ end
23
+
12
24
  def monkey_test(gestures)
25
+ monkey_test_precondition
13
26
  app_elements = describe_ui.shuffle
14
27
  current_time = Time.now
15
- while Time.now < current_time + duration
28
+ while Time.now < current_time + session_duration
16
29
  el1_coordinates = central_coordinates(app_elements.first)
17
30
  el2_coordinates = central_coordinates(app_elements.last)
18
31
  case gestures.sample
@@ -40,7 +53,32 @@ class Driver
40
53
  next
41
54
  end
42
55
  app_elements = describe_ui.shuffle
43
- Logger.error('App lost') if app_elements.include?(@home_tracker)
56
+ next unless app_elements.include?(@home_tracker)
57
+
58
+ save_session
59
+ Logger.error('App lost')
60
+ end
61
+ save_session
62
+ end
63
+
64
+ def repeat_monkey_test
65
+ monkey_test_precondition
66
+ session_actions.each do |action|
67
+ case action['type']
68
+ when 'tap'
69
+ tap(coordinates: { x: action['x'], y: action['y'] })
70
+ when 'press'
71
+ press(coordinates: { x: action['x'], y: action['y'] }, duration: action['duration'])
72
+ when 'swipe'
73
+ swipe(
74
+ start_coordinates: { x: action['x'], y: action['y'] },
75
+ end_coordinates: { x: action['endX'], y: action['endY'] },
76
+ duration: action['duration']
77
+ )
78
+ else
79
+ next
80
+ end
81
+ Logger.error('App lost') if describe_ui.shuffle.include?(@home_tracker)
44
82
  end
45
83
  end
46
84
 
@@ -84,40 +122,40 @@ class Driver
84
122
  end
85
123
 
86
124
  def list_targets
87
- @list_targets ||= `idb list-targets`.split("\n")
88
- @list_targets
125
+ @targets ||= `idb list-targets --json`.split("\n").map! { |target| JSON.parse(target) }
126
+ @targets
89
127
  end
90
128
 
91
- def list_booted_simulators
92
- `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) }
93
131
  end
94
132
 
95
133
  def ensure_app_installed
96
- 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}")
97
137
  end
98
138
 
99
139
  def ensure_device_exists
100
- device = list_targets.detect { |target| target.include?(udid) }
140
+ device = list_targets.detect { |target| target['udid'] == udid }
101
141
  Logger.error("Can't find device #{udid}") if device.nil?
102
142
 
103
- Logger.info('Device info:', payload: device)
104
- if device.include?('simulator')
143
+ Logger.info('Device info:', payload: JSON.pretty_generate(device))
144
+ if device['type'] == 'simulator'
105
145
  configure_simulator_keyboard
106
146
  boot_simulator
107
147
  end
108
148
  end
109
149
 
110
- def list_apps
111
- `idb list-apps --udid #{udid}`
112
- end
113
-
114
150
  def tap(coordinates:)
115
151
  Logger.info('Tap:', payload: JSON.pretty_generate(coordinates))
152
+ @session[:actions] << { type: :tap, x: coordinates[:x], y: coordinates[:y] } unless session_actions
116
153
  `idb ui tap --udid #{udid} #{coordinates[:x]} #{coordinates[:y]}`
117
154
  end
118
155
 
119
156
  def press(coordinates:, duration:)
120
157
  Logger.info("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
158
+ @session[:actions] << { type: :press, x: coordinates[:x], y: coordinates[:y], duration: duration } unless session_actions
121
159
  `idb ui tap --udid #{udid} --duration #{duration} #{coordinates[:x]} #{coordinates[:y]}`
122
160
  end
123
161
 
@@ -126,6 +164,16 @@ class Driver
126
164
  "Swipe (#{duration}s):",
127
165
  payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}"
128
166
  )
167
+ unless session_actions
168
+ @session[:actions] << {
169
+ type: :swipe,
170
+ x: start_coordinates[:x],
171
+ y: start_coordinates[:y],
172
+ endX: end_coordinates[:x],
173
+ endY: end_coordinates[:y],
174
+ duration: duration
175
+ }
176
+ end
129
177
  coordinates = "#{start_coordinates[:x]} #{start_coordinates[:y]} #{end_coordinates[:x]} #{end_coordinates[:y]}"
130
178
  `idb ui swipe --udid #{udid} --duration #{duration} #{coordinates}`
131
179
  end
@@ -168,6 +216,10 @@ class Driver
168
216
  rand(0.5..1.5).ceil(1)
169
217
  end
170
218
 
219
+ def save_session
220
+ File.write("#{session_path}/xcmonkey-session.json", JSON.pretty_generate(@session))
221
+ end
222
+
171
223
  private
172
224
 
173
225
  def ensure_driver_installed
@@ -183,14 +235,13 @@ class Driver
183
235
  end
184
236
 
185
237
  def wait_until_app_launched
186
- app_info = nil
238
+ app_is_running = false
187
239
  current_time = Time.now
188
- while app_info.nil? && Time.now < current_time + 5
189
- app_info = list_apps.split("\n").detect do |app|
190
- app =~ /#{bundle_id}.*Running/
191
- 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'
192
243
  end
193
- Logger.error("Can't run the app #{bundle_id}") if app_info.nil?
194
- 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))
195
246
  end
196
247
  end
@@ -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
@@ -1,4 +1,3 @@
1
- module Xcmonkey
2
- VERSION = '0.3.0'
3
- GEM_NAME = 'xcmonkey'
1
+ class Xcmonkey
2
+ VERSION = '1.1.0'
4
3
  end
data/lib/xcmonkey.rb CHANGED
@@ -1,44 +1,42 @@
1
1
  require 'json'
2
2
  require 'colorize'
3
3
  require_relative 'xcmonkey/describer'
4
+ require_relative 'xcmonkey/repeater'
4
5
  require_relative 'xcmonkey/version'
5
6
  require_relative 'xcmonkey/logger'
6
7
  require_relative 'xcmonkey/driver'
7
8
 
8
- module Xcmonkey
9
- class Xcmonkey
10
- attr_accessor :udid, :bundle_id, :duration, :driver
11
-
12
- def initialize(params)
13
- ensure_required_params(params)
14
- self.udid = params[:udid]
15
- self.bundle_id = params[:bundle_id]
16
- self.duration = params[:duration]
17
- self.driver = Driver.new(params)
18
- end
19
-
20
- 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
26
- driver.monkey_test(gestures)
27
- end
9
+ class Xcmonkey
10
+ attr_accessor :driver
11
+
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
19
+
20
+ def run
21
+ driver.monkey_test(gestures)
22
+ end
23
+
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
- def gestures
30
- taps = [:precise_tap, :blind_tap] * 10
31
- swipes = [:precise_swipe, :blind_swipe] * 5
32
- presses = [:precise_press, :blind_press]
33
- taps + swipes + presses
34
- end
35
-
36
- def ensure_required_params(params)
37
- Logger.error('UDID should be provided') if params[:udid].nil?
38
- Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
39
- if params[:duration].nil? || !params[:duration].kind_of?(Integer) || !params[:duration].positive?
40
- Logger.error('Duration must be Integer and not less than 1 second')
41
- end
42
- end
43
- end
31
+ def ensure_required_params(params)
32
+ Logger.error('UDID should be provided') if params[:udid].nil?
33
+
34
+ Logger.error('Bundle identifier should be provided') if params[:bundle_id].nil?
35
+
36
+ Logger.error('Session path should be a directory') if params[:session_path].nil? || !File.directory?(params[:session_path])
37
+
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
44
42
  end
data/spec/driver_spec.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  describe Driver do
2
2
  let(:udid) { `xcrun simctl list | grep " iPhone 14 Pro Max"`.split("\n")[0].split('(')[1].split(')')[0] }
3
3
  let(:bundle_id) { 'com.apple.Maps' }
4
- let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id) }
4
+ let(:driver) { described_class.new(udid: udid, bundle_id: bundle_id, session_path: Dir.pwd) }
5
+ let(:driver_with_session) { described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ type: 'tap', x: 0, y: 0 }]) }
5
6
 
6
7
  it 'verifies that sumulator was booted' do
7
8
  error_message = "Failed to boot #{udid}"
@@ -9,12 +10,6 @@ describe Driver do
9
10
  expect { driver.boot_simulator }.not_to raise_error
10
11
  end
11
12
 
12
- it 'verifies that there are booted simulators' do
13
- driver.boot_simulator
14
- booted_simulators = driver.list_booted_simulators
15
- expect(booted_simulators).not_to be_empty
16
- end
17
-
18
13
  it 'verifies that ui can be described' do
19
14
  driver.boot_simulator
20
15
  ui = driver.describe_ui
@@ -40,8 +35,8 @@ describe Driver do
40
35
 
41
36
  it 'verifies that list of apps can be showed' do
42
37
  driver.boot_simulator
43
- list_apps = driver.list_apps
44
- expect(list_apps).to include(bundle_id)
38
+ app_exists = driver.list_apps.any? { |app| app['bundle_id'] == bundle_id }
39
+ expect(app_exists).to be(true)
45
40
  end
46
41
 
47
42
  it 'verifies that app installed' do
@@ -61,11 +56,11 @@ describe Driver do
61
56
  end
62
57
 
63
58
  it 'verifies that device exists' do
64
- error_message = "Can't find device #{udid}"
65
- payload = driver.list_targets.detect { |target| target.include?(udid) }
66
- expect(Logger).not_to receive(:error).with(error_message, payload: nil)
67
- expect(Logger).to receive(:info).with('Device info:', payload: payload)
59
+ payload = driver.list_targets.detect { |target| target['udid'] == udid }
60
+ expect(Logger).not_to receive(:error)
61
+ expect(Logger).to receive(:info).with('Device info:', payload: JSON.pretty_generate(payload))
68
62
  expect(driver).to receive(:boot_simulator)
63
+ expect(driver).to receive(:configure_simulator_keyboard)
69
64
  expect { driver.ensure_device_exists }.not_to raise_error
70
65
  end
71
66
 
@@ -150,28 +145,106 @@ describe Driver do
150
145
  expect { driver.launch_app }.not_to raise_error
151
146
  end
152
147
 
153
- it 'verifies tap' do
148
+ it 'verifies tap in new session' do
154
149
  driver.boot_simulator
155
150
  coordinates = { x: 1, y: 1 }
156
151
  expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
157
152
  driver.tap(coordinates: coordinates)
153
+ expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
154
+ end
155
+
156
+ it 'verifies tap in old session' do
157
+ driver_with_session.boot_simulator
158
+ coordinates = { x: 1, y: 1 }
159
+ expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
160
+ driver_with_session.tap(coordinates: coordinates)
161
+ expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
158
162
  end
159
163
 
160
- it 'verifies press' do
164
+ it 'verifies press in new session' do
161
165
  driver.boot_simulator
162
166
  duration = 0.5
163
167
  coordinates = { x: 1, y: 1 }
164
168
  expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
165
169
  driver.press(coordinates: coordinates, duration: duration)
170
+ expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
171
+ end
172
+
173
+ it 'verifies press in old session' do
174
+ driver_with_session.boot_simulator
175
+ duration = 0.5
176
+ coordinates = { x: 1, y: 1 }
177
+ expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
178
+ driver_with_session.press(coordinates: coordinates, duration: duration)
179
+ expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
166
180
  end
167
181
 
168
- it 'verifies swipe' do
182
+ it 'verifies swipe in new session' do
169
183
  driver.boot_simulator
170
184
  duration = 0.5
171
185
  start_coordinates = { x: 1, y: 1 }
172
186
  end_coordinates = { x: 2, y: 2 }
173
187
  expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
174
188
  driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
189
+ expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
190
+ end
191
+
192
+ it 'verifies swipe in old session' do
193
+ driver_with_session.boot_simulator
194
+ duration = 0.5
195
+ start_coordinates = { x: 1, y: 1 }
196
+ end_coordinates = { x: 2, y: 2 }
197
+ expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
198
+ driver_with_session.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
199
+ expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
200
+ end
201
+
202
+ it 'verifies that session can be saved' do
203
+ expect(File).to receive(:write)
204
+ driver.instance_variable_set(:@session, { params: {}, actions: [] })
205
+ driver.save_session
206
+ end
207
+
208
+ it 'verifies that monkey_test_precondition works fine' do
209
+ driver.monkey_test_precondition
210
+ app_info = driver.list_apps.detect { |app| app['bundle_id'] == bundle_id }
211
+ app_is_running = app_info && app_info['process_state'] == 'Running'
212
+ expect(app_is_running).to be(true)
213
+ end
214
+
215
+ it 'verifies that monkey_test works fine' do
216
+ params = { udid: udid, bundle_id: bundle_id, duration: 1, session_path: Dir.pwd }
217
+ driver = described_class.new(params)
218
+ expect(driver).to receive(:monkey_test_precondition)
219
+ driver.monkey_test(Xcmonkey.new(params).gestures)
220
+ expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
221
+ end
222
+
223
+ it 'verifies that repeat_monkey_test works fine' do
224
+ session_actions = [
225
+ { 'type' => 'tap', 'x' => 10, 'y' => 10 },
226
+ { 'type' => 'press', 'x' => 11, 'y' => 11, 'duration' => 1.4 },
227
+ { 'type' => 'swipe', 'x' => 12, 'y' => 12, 'endX' => 15, 'endY' => 15, 'duration' => 0.3 }
228
+ ]
229
+ driver = described_class.new(udid: udid, bundle_id: bundle_id, session_actions: session_actions)
230
+ allow(Logger).to receive(:info).twice
231
+ expect(driver).to receive(:monkey_test_precondition)
232
+ expect(driver).to receive(:tap).with(coordinates: { x: 10, y: 10 })
233
+ expect(driver).to receive(:press).with(coordinates: { x: 11, y: 11 }, duration: 1.4)
234
+ expect(driver).to receive(:swipe).with(start_coordinates: { x: 12, y: 12 }, end_coordinates: { x: 15, y: 15 }, duration: 0.3)
235
+ driver.repeat_monkey_test
236
+ expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
237
+ end
238
+
239
+ it 'verifies that unknown actions does not break repeat_monkey_test' do
240
+ driver = described_class.new(udid: udid, bundle_id: bundle_id, session_actions: [{ 'type' => 'test', 'x' => 10, 'y' => 10 }])
241
+ allow(Logger).to receive(:info).twice
242
+ expect(driver).to receive(:monkey_test_precondition)
243
+ expect(driver).not_to receive(:tap)
244
+ expect(driver).not_to receive(:press)
245
+ expect(driver).not_to receive(:swipe)
246
+ driver.repeat_monkey_test
247
+ expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
175
248
  end
176
249
 
177
250
  it 'verifies that simulator was not booted' do
@@ -0,0 +1,51 @@
1
+ describe Repeater do
2
+ let(:session_path) { 'test/path/session.json' }
3
+ let(:session_file_content_full) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
4
+ let(:session_file_content_without_params) { '{ "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
5
+ let(:session_file_content_with_empty_actions) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [] }' }
6
+ let(:session_file_content_without_actions) { '{ "params": {"udid": "0", "bundle_id": "0", "enable_simulator_keyboard": true} }' }
7
+ let(:session_file_content_without_bundle_id) { '{ "params": {"udid": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
8
+ let(:session_file_content_without_udid) { '{ "params": {"bundle_id": "0", "enable_simulator_keyboard": true}, "actions": [{ "type": "tap", "x": 0, "y": 0 }] }' }
9
+
10
+ it 'verifies that session cannot be validated without params' do
11
+ allow(File).to receive(:exist?).and_return(true)
12
+ allow(File).to receive(:read).and_return(session_file_content_without_params)
13
+ expect(Logger).to receive(:error).with('Provided session is not valid: `params` should not be `nil`')
14
+ described_class.new(session_path: session_path)
15
+ end
16
+
17
+ it 'verifies that session cannot be validated without actions' do
18
+ allow(File).to receive(:exist?).and_return(true)
19
+ allow(File).to receive(:read).and_return(session_file_content_without_actions)
20
+ expect(Logger).to receive(:error).with('Provided session is not valid: `actions` should not be `nil` or `empty`')
21
+ described_class.new(session_path: session_path)
22
+ end
23
+
24
+ it 'verifies that session cannot be validated with empty actions' do
25
+ allow(File).to receive(:exist?).and_return(true)
26
+ allow(File).to receive(:read).and_return(session_file_content_with_empty_actions)
27
+ expect(Logger).to receive(:error).with('Provided session is not valid: `actions` should not be `nil` or `empty`')
28
+ described_class.new(session_path: session_path)
29
+ end
30
+
31
+ it 'verifies that session cannot be validated without bundle id' do
32
+ allow(File).to receive(:exist?).and_return(true)
33
+ allow(File).to receive(:read).and_return(session_file_content_without_bundle_id)
34
+ expect(Logger).to receive(:error).with('Provided session is not valid: `bundle_id` should not be `nil`')
35
+ described_class.new(session_path: session_path)
36
+ end
37
+
38
+ it 'verifies that session cannot be validated without udid' do
39
+ allow(File).to receive(:exist?).and_return(true)
40
+ allow(File).to receive(:read).and_return(session_file_content_without_udid)
41
+ expect(Logger).to receive(:error).with('Provided session is not valid: `udid` should not be `nil`')
42
+ described_class.new(session_path: session_path)
43
+ end
44
+
45
+ it 'verifies that session validation can pass' do
46
+ allow(File).to receive(:exist?).and_return(true)
47
+ allow(File).to receive(:read).and_return(session_file_content_full)
48
+ expect(Logger).not_to receive(:error)
49
+ described_class.new(session_path: session_path)
50
+ end
51
+ end
@@ -1,58 +1,52 @@
1
1
  describe Xcmonkey do
2
- describe Xcmonkey::Xcmonkey do
3
- let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10 } }
4
- let(:duration_error_msg) { 'Duration must be Integer and not less than 1 second' }
5
-
6
- it 'verifies gestures' do
7
- gestures = described_class.new(params).gestures
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
12
- end
13
-
14
- it 'verifies required params' do
15
- expect(Logger).not_to receive(:error)
16
- described_class.new(params)
17
- end
18
-
19
- it 'verifies `udid` param is required' do
20
- params[:udid] = nil
21
- expect(Logger).to receive(:error).with('UDID should be provided')
22
- described_class.new(params)
23
- end
24
-
25
- it 'verifies `bundle_id` param is required' do
26
- params[:bundle_id] = nil
27
- expect(Logger).to receive(:error).with('Bundle identifier should be provided')
28
- described_class.new(params)
29
- end
30
-
31
- it 'verifies `duration` param is required' do
32
- params[:duration] = nil
33
- expect(Logger).to receive(:error).with(duration_error_msg)
34
- described_class.new(params)
35
- end
36
-
37
- it 'verifies `duration` param cannot be equal to zero' do
38
- params[:duration] = 0
39
- expect(Logger).to receive(:error).with(duration_error_msg)
40
- described_class.new(params)
41
- end
42
-
43
- it 'verifies `duration` param cannot be negative' do
44
- params[:duration] = -1
45
- expect(Logger).to receive(:error).with(duration_error_msg)
46
- described_class.new(params)
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
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
+ it 'verifies gestures' do
6
+ gestures = described_class.new(params).gestures
7
+ taps = [:precise_tap, :blind_tap] * 10
8
+ swipes = [:precise_swipe, :blind_swipe] * 5
9
+ presses = [:precise_press, :blind_press]
10
+ expect(gestures) =~ presses + taps + swipes
11
+ end
12
+
13
+ it 'verifies required params' do
14
+ expect(Logger).not_to receive(:error)
15
+ described_class.new(params)
16
+ end
17
+
18
+ it 'verifies `udid` param is required' do
19
+ params[:udid] = nil
20
+ expect(Logger).to receive(:error).with('UDID should be provided')
21
+ described_class.new(params)
22
+ end
23
+
24
+ it 'verifies `bundle_id` param is required' do
25
+ params[:bundle_id] = nil
26
+ expect(Logger).to receive(:error).with('Bundle identifier should be provided')
27
+ described_class.new(params)
28
+ end
29
+
30
+ it 'verifies `duration` param is optional' do
31
+ params[:duration] = nil
32
+ expect(Logger).not_to receive(:error)
33
+ described_class.new(params)
34
+ end
35
+
36
+ it 'verifies `duration` param cannot be equal to zero' do
37
+ params[:duration] = 0
38
+ expect(Logger).to receive(:error).with(duration_error_msg)
39
+ described_class.new(params)
40
+ end
41
+
42
+ it 'verifies `duration` param cannot be negative' do
43
+ params[:duration] = -1
44
+ expect(Logger).to receive(:error).with(duration_error_msg)
45
+ described_class.new(params)
46
+ end
47
+
48
+ it 'verifies version' do
49
+ current_version = Gem::Version.new(Xcmonkey::VERSION)
50
+ expect(current_version).to be > Gem::Version.new('0.1.0')
57
51
  end
58
52
  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 = Xcmonkey::GEM_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: 0.3.0
4
+ version: 1.1.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-08 00:00:00.000000000 Z
11
+ date: 2023-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -216,6 +216,8 @@ 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
222
  - ".github/pull_request_template.md"
221
223
  - ".github/workflows/test.yml"
@@ -236,12 +238,14 @@ files:
236
238
  - lib/xcmonkey/describer.rb
237
239
  - lib/xcmonkey/driver.rb
238
240
  - lib/xcmonkey/logger.rb
241
+ - lib/xcmonkey/repeater.rb
239
242
  - lib/xcmonkey/version.rb
240
243
  - requirements.txt
241
244
  - sonar-project.properties
242
245
  - spec/describer_spec.rb
243
246
  - spec/driver_spec.rb
244
247
  - spec/logger_spec.rb
248
+ - spec/repeater_spec.rb
245
249
  - spec/spec_helper.rb
246
250
  - spec/xcmonkey_spec.rb
247
251
  - xcmonkey.gemspec