xcmonkey 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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"