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