xcmonkey 1.1.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: 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