xcmonkey 0.3.0 → 1.1.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 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