xcmonkey 1.1.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: 7d3bd605b57ec05a83a3cca39707b5c2fc9529828131b9ac872f3dc478180bbb
4
- data.tar.gz: 8793e64f73775a2c8de84d1b79d0aebd258df7c22b9c8f43de2618f63f306d4e
3
+ metadata.gz: c93c94403332c085a393877d88d354b2736788f6e52d118eb8b9faa70c91b1e5
4
+ data.tar.gz: ee0f3bfcd014151b4821a6df50f601205581ebd480b5626e33588bda2928981a
5
5
  SHA512:
6
- metadata.gz: 4e4634518e71dac0ed206d4fec26fc707fb9dbea8116c87db1959695261e8ee311246db203317790433d18b5e5250074f1f17bf567101dfec6767a1ed5db0670
7
- data.tar.gz: 0e696e9804b94e6106f203b9bcda008f57dbe7fbda78a68ff7d57a8cfa980e4b691b533f430bac855ae8ed56ae0e7ac5085f305853bae8f03652095871287a94
6
+ metadata.gz: 0ab88db343b0e83363130a9e9c8ed965acbf9d58f56d597b301ff12da4df9d4e79b08c3b2284b870a65c9bfc3d05a8ba9df0d19409631620a28836c9d5785814
7
+ data.tar.gz: fe46af5c215273be9210f3c880617457a12c6063e0bd1901d6abbd10750f10935b77a4797ff374926c3f1cac6b1ef79032f210fb705e05917146e4c64cede97c
data/README.md CHANGED
@@ -39,7 +39,7 @@ 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
42
+ $ xcmonkey test --duration 100 --bundle-id "com.apple.Maps" --udid "413EA256-CFFB-4312-94A6-12592BEE4CBA"
43
43
 
44
44
  12:44:19.343: Device info: {
45
45
  "name": "iPhone 14 Pro",
@@ -106,9 +106,9 @@ require 'xcmonkey'
106
106
 
107
107
  lane :test do
108
108
  Xcmonkey.new(
109
- udid: '413EA256-CFFB-4312-94A6-12592BEE4CBA',
109
+ duration: 100,
110
110
  bundle_id: 'com.apple.Maps',
111
- duration: 100
111
+ udid: '413EA256-CFFB-4312-94A6-12592BEE4CBA'
112
112
  ).run
113
113
  end
114
114
  ```
@@ -16,15 +16,16 @@ class Driver
16
16
  puts
17
17
  ensure_device_exists
18
18
  ensure_app_installed
19
- terminate_app
20
- open_home_screen(with_tracker: true)
21
- 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
22
22
  end
23
23
 
24
24
  def monkey_test(gestures)
25
25
  monkey_test_precondition
26
26
  app_elements = describe_ui.shuffle
27
27
  current_time = Time.now
28
+ counter = 0
28
29
  while Time.now < current_time + session_duration
29
30
  el1_coordinates = central_coordinates(app_elements.first)
30
31
  el2_coordinates = central_coordinates(app_elements.last)
@@ -52,17 +53,17 @@ class Driver
52
53
  else
53
54
  next
54
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
55
59
  app_elements = describe_ui.shuffle
56
- next unless app_elements.include?(@home_tracker)
57
-
58
- save_session
59
- Logger.error('App lost')
60
60
  end
61
61
  save_session
62
62
  end
63
63
 
64
64
  def repeat_monkey_test
65
65
  monkey_test_precondition
66
+ counter = 0
66
67
  session_actions.each do |action|
67
68
  case action['type']
68
69
  when 'tap'
@@ -78,15 +79,12 @@ class Driver
78
79
  else
79
80
  next
80
81
  end
81
- 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
82
85
  end
83
86
  end
84
87
 
85
- def open_home_screen(with_tracker: false)
86
- `idb ui button --udid #{udid} HOME`
87
- detect_home_unique_element if with_tracker
88
- end
89
-
90
88
  def describe_ui
91
89
  JSON.parse(`idb ui describe-all --udid #{udid}`)
92
90
  end
@@ -97,13 +95,13 @@ class Driver
97
95
  point_info
98
96
  end
99
97
 
100
- def launch_app
101
- `idb launch --udid #{udid} #{bundle_id}`
102
- 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
103
101
  end
104
102
 
105
- def terminate_app
106
- `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`
107
105
  end
108
106
 
109
107
  def boot_simulator
@@ -130,6 +128,10 @@ class Driver
130
128
  `idb list-apps --udid #{udid} --json`.split("\n").map! { |app| JSON.parse(app) }
131
129
  end
132
130
 
131
+ def list_running_apps
132
+ list_apps.select { |app| app['process_state'] == 'Running' }
133
+ end
134
+
133
135
  def ensure_app_installed
134
136
  return if list_apps.any? { |app| app['bundle_id'] == bundle_id }
135
137
 
@@ -144,6 +146,9 @@ class Driver
144
146
  if device['type'] == 'simulator'
145
147
  configure_simulator_keyboard
146
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')
147
152
  end
148
153
  end
149
154
 
@@ -220,28 +225,57 @@ class Driver
220
225
  File.write("#{session_path}/xcmonkey-session.json", JSON.pretty_generate(@session))
221
226
  end
222
227
 
223
- 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
224
235
 
225
- def ensure_driver_installed
226
- 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
227
244
  end
228
245
 
229
- def detect_home_unique_element
230
- @home_tracker ||= describe_ui.reverse.detect do |el|
231
- sleep(1)
232
- !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")
233
257
  end
234
- @home_tracker
235
258
  end
236
259
 
237
- def wait_until_app_launched
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)
238
272
  app_is_running = false
239
273
  current_time = Time.now
240
274
  while !app_is_running && Time.now < current_time + 5
241
- app_info = list_apps.detect { |app| app['bundle_id'] == bundle_id }
275
+ app_info = list_apps.detect { |app| app['bundle_id'] == target_bundle_id }
242
276
  app_is_running = app_info && app_info['process_state'] == 'Running'
243
277
  end
244
- Logger.error("Can't run the app #{bundle_id}") unless app_is_running
278
+ Logger.error("Can't run the app #{target_bundle_id}") unless app_is_running
245
279
  Logger.info('App info:', payload: JSON.pretty_generate(app_info))
246
280
  end
247
281
  end
@@ -1,3 +1,3 @@
1
1
  class Xcmonkey
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  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,6 +4,10 @@ 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)
@@ -16,18 +20,6 @@ describe Driver do
16
20
  expect(ui).not_to be_empty
17
21
  end
18
22
 
19
- it 'verifies that home screen can be opened' do
20
- driver.boot_simulator
21
- home_tracker = driver.open_home_screen(with_tracker: true)
22
- expect(home_tracker).not_to be_empty
23
- end
24
-
25
- it 'verifies that home screen can be opened without tracker' do
26
- driver.boot_simulator
27
- home_tracker = driver.open_home_screen(with_tracker: false)
28
- expect(home_tracker).to be_nil
29
- end
30
-
31
23
  it 'verifies that list of targets can be showed' do
32
24
  list_targets = driver.list_targets
33
25
  expect(list_targets).not_to be_empty
@@ -56,9 +48,7 @@ describe Driver do
56
48
  end
57
49
 
58
50
  it 'verifies that device exists' do
59
- payload = driver.list_targets.detect { |target| target['udid'] == udid }
60
51
  expect(Logger).not_to receive(:error)
61
- expect(Logger).to receive(:info).with('Device info:', payload: JSON.pretty_generate(payload))
62
52
  expect(driver).to receive(:boot_simulator)
63
53
  expect(driver).to receive(:configure_simulator_keyboard)
64
54
  expect { driver.ensure_device_exists }.not_to raise_error
@@ -137,18 +127,25 @@ describe Driver do
137
127
  expect(keyboard_state.first).to include('1')
138
128
  end
139
129
 
140
- 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
141
139
  expect(Logger).not_to receive(:error)
142
- expect(Logger).to receive(:info)
140
+ expect(driver).not_to receive(:wait_until_app_launched)
143
141
  driver.boot_simulator
144
- driver.terminate_app
145
- 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
146
144
  end
147
145
 
148
146
  it 'verifies tap in new session' do
149
147
  driver.boot_simulator
150
148
  coordinates = { x: 1, y: 1 }
151
- expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
152
149
  driver.tap(coordinates: coordinates)
153
150
  expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
154
151
  end
@@ -156,7 +153,6 @@ describe Driver do
156
153
  it 'verifies tap in old session' do
157
154
  driver_with_session.boot_simulator
158
155
  coordinates = { x: 1, y: 1 }
159
- expect(Logger).to receive(:info).with('Tap:', payload: JSON.pretty_generate(coordinates))
160
156
  driver_with_session.tap(coordinates: coordinates)
161
157
  expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
162
158
  end
@@ -165,7 +161,6 @@ describe Driver do
165
161
  driver.boot_simulator
166
162
  duration = 0.5
167
163
  coordinates = { x: 1, y: 1 }
168
- expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
169
164
  driver.press(coordinates: coordinates, duration: duration)
170
165
  expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
171
166
  end
@@ -174,7 +169,6 @@ describe Driver do
174
169
  driver_with_session.boot_simulator
175
170
  duration = 0.5
176
171
  coordinates = { x: 1, y: 1 }
177
- expect(Logger).to receive(:info).with("Press (#{duration}s):", payload: JSON.pretty_generate(coordinates))
178
172
  driver_with_session.press(coordinates: coordinates, duration: duration)
179
173
  expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
180
174
  end
@@ -184,7 +178,6 @@ describe Driver do
184
178
  duration = 0.5
185
179
  start_coordinates = { x: 1, y: 1 }
186
180
  end_coordinates = { x: 2, y: 2 }
187
- expect(Logger).to receive(:info).with("Swipe (#{duration}s):", payload: "#{JSON.pretty_generate(start_coordinates)} => #{JSON.pretty_generate(end_coordinates)}")
188
181
  driver.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
189
182
  expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
190
183
  end
@@ -194,7 +187,6 @@ describe Driver do
194
187
  duration = 0.5
195
188
  start_coordinates = { x: 1, y: 1 }
196
189
  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
190
  driver_with_session.swipe(start_coordinates: start_coordinates, end_coordinates: end_coordinates, duration: duration)
199
191
  expect(driver_with_session.instance_variable_get(:@session)[:actions]).to be_empty
200
192
  end
@@ -210,12 +202,12 @@ describe Driver do
210
202
  app_info = driver.list_apps.detect { |app| app['bundle_id'] == bundle_id }
211
203
  app_is_running = app_info && app_info['process_state'] == 'Running'
212
204
  expect(app_is_running).to be(true)
205
+ expect(driver.instance_variable_get(:@running_apps)).not_to be_nil
213
206
  end
214
207
 
215
208
  it 'verifies that monkey_test works fine' do
216
209
  params = { udid: udid, bundle_id: bundle_id, duration: 1, session_path: Dir.pwd }
217
210
  driver = described_class.new(params)
218
- expect(driver).to receive(:monkey_test_precondition)
219
211
  driver.monkey_test(Xcmonkey.new(params).gestures)
220
212
  expect(driver.instance_variable_get(:@session)[:actions]).not_to be_empty
221
213
  end
@@ -227,8 +219,6 @@ describe Driver do
227
219
  { 'type' => 'swipe', 'x' => 12, 'y' => 12, 'endX' => 15, 'endY' => 15, 'duration' => 0.3 }
228
220
  ]
229
221
  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
222
  expect(driver).to receive(:tap).with(coordinates: { x: 10, y: 10 })
233
223
  expect(driver).to receive(:press).with(coordinates: { x: 11, y: 11 }, duration: 1.4)
234
224
  expect(driver).to receive(:swipe).with(start_coordinates: { x: 12, y: 12 }, end_coordinates: { x: 15, y: 15 }, duration: 0.3)
@@ -238,7 +228,6 @@ describe Driver do
238
228
 
239
229
  it 'verifies that unknown actions does not break repeat_monkey_test' do
240
230
  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
231
  expect(driver).to receive(:monkey_test_precondition)
243
232
  expect(driver).not_to receive(:tap)
244
233
  expect(driver).not_to receive(:press)
@@ -247,6 +236,59 @@ describe Driver do
247
236
  expect(driver.instance_variable_get(:@session)[:actions]).to be_empty
248
237
  end
249
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
+
250
292
  it 'verifies that simulator was not booted' do
251
293
  driver.shutdown_simulator
252
294
  error_message = "Failed to boot #{udid}"
@@ -2,6 +2,10 @@ describe Xcmonkey do
2
2
  let(:params) { { udid: '123', bundle_id: 'example.com.app', duration: 10, session_path: Dir.pwd } }
3
3
  let(:duration_error_msg) { 'Duration must be Integer and not less than 1 second' }
4
4
 
5
+ before do
6
+ allow(Logger).to receive(:info)
7
+ end
8
+
5
9
  it 'verifies gestures' do
6
10
  gestures = described_class.new(params).gestures
7
11
  taps = [:precise_tap, :blind_tap] * 10
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.1.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-20 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