run_loop 2.1.9 → 2.1.10

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
  SHA1:
3
- metadata.gz: 27742f67d963e5adb2a2593eacf8a2c2424fa91d
4
- data.tar.gz: d45659fcb0afe6f1ca718258f79151855b3de433
3
+ metadata.gz: f1d5ec2c34563d9302dd2ea711a02542f391cdb1
4
+ data.tar.gz: 0559160c9bba7c824d069f8b1f90819328033691
5
5
  SHA512:
6
- metadata.gz: 7740754d0a410b6309f93b7798182a439cf792d6abaa2861dc6a163a444937e39853fb0e550fa6e4b025cc575a265628f0f52df3a0d338a86da3bd5060cf37ef
7
- data.tar.gz: 52faa616a1eee66cedf4889bac0b1d6b43f6fce7f52574b635b2e4bf1281c8445ba6e8889819af1b1c4fd4a8d2dd9309f112ccb9be3983dd1e3165f59aefea04
6
+ metadata.gz: 46181f7467e976ef0f53d874b5145de368055be6d3fac52e6e872c1f28a1a39f9c04de92df0b4bcbfce270ef6b1438f1c1f590246cd51bc796c32acf54ec8c9b
7
+ data.tar.gz: e11a8e89c0a0ee501a961342d6e71705a3ebc9d1788d8ee23adc2fadeb4ac1ea321e44390df8517e753b81535eefe6332084ae63de9786ea24a9e613871a38b1
@@ -21,7 +21,7 @@ module RunLoop
21
21
  puts "No simulator for active Xcode (version #{version}) is booted."
22
22
  else
23
23
  log_file = device.simulator_log_file_path
24
- exec('tail', *['-F', log_file])
24
+ exec('tail', *["-n", "5000", '-F', log_file])
25
25
  end
26
26
  end
27
27
  end
@@ -342,7 +342,10 @@ class RunLoop::CoreSimulator
342
342
  end
343
343
 
344
344
  # Launch the simulator indicated by device.
345
- def launch_simulator
345
+ def launch_simulator(options={})
346
+ merged_options = {
347
+ :wait_for_stable => true
348
+ }.merge(options)
346
349
 
347
350
  if running_simulator_pid != nil
348
351
  # There is a running simulator.
@@ -370,7 +373,9 @@ class RunLoop::CoreSimulator
370
373
  options = { :timeout => 5, :raise_on_timeout => true }
371
374
  RunLoop::ProcessWaiter.new(sim_name, options).wait_for_any
372
375
 
373
- device.simulator_wait_for_stable_state
376
+ if merged_options[:wait_for_stable]
377
+ device.simulator_wait_for_stable_state
378
+ end
374
379
 
375
380
  elapsed = Time.now - start_time
376
381
  RunLoop.log_debug("Took #{elapsed} seconds to launch the simulator")
@@ -591,7 +596,8 @@ Command had no output
591
596
  timeout = DEFAULT_OPTIONS[:install_app_timeout]
592
597
  simctl.install(device, app, timeout)
593
598
 
594
- device.simulator_wait_for_stable_state
599
+ # Experimental: don't wait after the install
600
+ # device.simulator_wait_for_stable_state
595
601
  installed_app_bundle_dir
596
602
  end
597
603
 
@@ -19,14 +19,50 @@ module RunLoop
19
19
  # @!visibility private
20
20
  #
21
21
  # These defaults may change at any time.
22
+ #
23
+ # You can override these values if they do not work in your environment.
24
+ #
25
+ # For cucumber users, the best place to override would be in your
26
+ # features/support/env.rb.
27
+ #
28
+ # For example:
29
+ #
30
+ # RunLoop::DeviceAgent::Client::DEFAULTS[:http_timeout] = 60
22
31
  DEFAULTS = {
23
32
  :port => 27753,
24
33
  :simulator_ip => "127.0.0.1",
25
- :http_timeout => RunLoop::Environment.ci? ? 120 : 10,
34
+ :http_timeout => (RunLoop::Environment.ci? || RunLoop::Environment.xtc?) ? 120 : 10,
26
35
  :route_version => "1.0",
36
+
37
+ # Ignored in the XTC.
38
+ # This key is subject to removal or changes
39
+ :device_agent_install_timeout => RunLoop::Environment.ci? ? 120 : 60,
40
+ # This value must always be false on the XTC.
41
+ # This is should only be used by gem maintainers or very advanced users.
27
42
  :shutdown_device_agent_before_launch => false
28
43
  }
29
44
 
45
+ # @!visibility private
46
+ #
47
+ # These defaults may change at any time.
48
+ #
49
+ # You can override these values if they do not work in your environment.
50
+ #
51
+ # For cucumber users, the best place to override would be in your
52
+ # features/support/env.rb.
53
+ #
54
+ # For example:
55
+ #
56
+ # RunLoop::DeviceAgent::Client::WAIT_DEFAULTS[:timeout] = 30
57
+ WAIT_DEFAULTS = {
58
+ timeout: (RunLoop::Environment.ci? ||
59
+ RunLoop::Environment.xtc?) ? 30 : 15,
60
+ # This key is subject to removal or changes.
61
+ retry_frequency: 0.1,
62
+ # This key is subject to removal or changes.
63
+ exception_class: Timeout::Error
64
+ }
65
+
30
66
  # @!visibility private
31
67
  def self.run(options={})
32
68
  # logger = options[:logger]
@@ -76,17 +112,30 @@ $ xcrun security find-identity -v -p codesigning
76
112
  end
77
113
  end
78
114
 
79
- launch_options = options.merge({:code_sign_identity => code_sign_identity})
80
- xcuitest = RunLoop::DeviceAgent::Client.new(bundle_id, device, cbx_launcher)
81
- xcuitest.launch(launch_options)
115
+ install_timeout = options.fetch(:device_agent_install_timeout,
116
+ DEFAULTS[:device_agent_install_timeout])
117
+ shutdown_before_launch = options.fetch(:shutdown_device_agent_before_launch,
118
+ DEFAULTS[:shutdown_device_agent_before_launch])
119
+
120
+ launcher_options = {
121
+ code_sign_identity: code_sign_identity,
122
+ device_agent_install_timeout: install_timeout,
123
+ shutdown_device_agent_before_launch: shutdown_before_launch
124
+ }
125
+
126
+ xcuitest = RunLoop::DeviceAgent::Client.new(bundle_id, device,
127
+ cbx_launcher, launcher_options)
128
+ xcuitest.launch
82
129
 
83
130
  if !RunLoop::Environment.xtc?
84
131
  cache = {
85
- :cbx_launcher => cbx_launcher.name,
86
132
  :udid => device.udid,
87
133
  :app => bundle_id,
88
134
  :automator => :device_agent,
89
- :code_sign_identity => code_sign_identity
135
+ :code_sign_identity => code_sign_identity,
136
+ :launcher => cbx_launcher.name,
137
+ :launcher_pid => xcuitest.launcher_pid,
138
+ :launcher_options => launcher_options
90
139
  }
91
140
  RunLoop::Cache.default.write(cache)
92
141
  end
@@ -119,7 +168,7 @@ $ xcrun security find-identity -v -p codesigning
119
168
  end
120
169
  end
121
170
 
122
- attr_reader :bundle_id, :device, :cbx_launcher, :launch_options
171
+ attr_reader :bundle_id, :device, :cbx_launcher, :launcher_options, :launcher_pid
123
172
 
124
173
  # @!visibility private
125
174
  #
@@ -129,10 +178,16 @@ $ xcrun security find-identity -v -p codesigning
129
178
  # @param [RunLoop::Device] device The device under test.
130
179
  # @param [RunLoop::DeviceAgent::LauncherStrategy] cbx_launcher The entity that
131
180
  # launches the CBXRunner.
132
- def initialize(bundle_id, device, cbx_launcher)
181
+ def initialize(bundle_id, device, cbx_launcher, launcher_options)
133
182
  @bundle_id = bundle_id
134
183
  @device = device
135
184
  @cbx_launcher = cbx_launcher
185
+ @launcher_options = launcher_options
186
+
187
+ if !@launcher_options[:device_agent_install_timeout]
188
+ default = DEFAULTS[:device_agent_install_timeout]
189
+ @launcher_options[:device_agent_install_timeout] = default
190
+ end
136
191
  end
137
192
 
138
193
  # @!visibility private
@@ -146,10 +201,9 @@ $ xcrun security find-identity -v -p codesigning
146
201
  end
147
202
 
148
203
  # @!visibility private
149
- def launch(options={})
150
- @launch_options = options
204
+ def launch
151
205
  start = Time.now
152
- launch_cbx_runner(options)
206
+ launch_cbx_runner
153
207
  launch_aut
154
208
  elapsed = Time.now - start
155
209
  RunLoop.log_debug("Took #{elapsed} seconds to launch #{bundle_id} on #{device}")
@@ -167,6 +221,11 @@ $ xcrun security find-identity -v -p codesigning
167
221
 
168
222
  # @!visibility private
169
223
  def stop
224
+ if RunLoop::Environment.xtc?
225
+ RunLoop.log_error("Calling shutdown on the XTC is not supported.")
226
+ return
227
+ end
228
+
170
229
  begin
171
230
  shutdown
172
231
  rescue => _
@@ -175,6 +234,8 @@ $ xcrun security find-identity -v -p codesigning
175
234
  end
176
235
 
177
236
  # @!visibility private
237
+ #
238
+ # Experimental!
178
239
  def launch_other_app(bundle_id)
179
240
  launch_aut(bundle_id)
180
241
  end
@@ -183,49 +244,31 @@ $ xcrun security find-identity -v -p codesigning
183
244
  def device_info
184
245
  options = http_options
185
246
  request = request("device")
186
- client = client(options)
247
+ client = http_client(options)
187
248
  response = client.get(request)
188
- expect_200_response(response)
249
+ expect_300_response(response)
189
250
  end
190
251
 
191
252
  # TODO Legacy API; remove once this branch is merged:
192
253
  # https://github.com/calabash/DeviceAgent.iOS/pull/133
193
254
  alias_method :runtime, :device_info
194
255
 
195
- # @!visibility private
196
- def server_pid
197
- options = http_options
198
- request = request("pid")
199
- client = client(options)
200
- response = client.get(request)
201
- expect_200_response(response)
202
- end
203
-
204
256
  # @!visibility private
205
257
  def server_version
206
258
  options = http_options
207
259
  request = request("version")
208
- client = client(options)
260
+ client = http_client(options)
209
261
  response = client.get(request)
210
- expect_200_response(response)
211
- end
212
-
213
- # @!visibility private
214
- def session_identifier
215
- options = http_options
216
- request = request("sessionIdentifier")
217
- client = client(options)
218
- response = client.get(request)
219
- expect_200_response(response)
262
+ expect_300_response(response)
220
263
  end
221
264
 
222
265
  # @!visibility private
223
266
  def tree
224
267
  options = http_options
225
268
  request = request("tree")
226
- client = client(options)
269
+ client = http_client(options)
227
270
  response = client.get(request)
228
- expect_200_response(response)
271
+ expect_300_response(response)
229
272
  end
230
273
 
231
274
  # @!visibility private
@@ -233,9 +276,9 @@ $ xcrun security find-identity -v -p codesigning
233
276
  options = http_options
234
277
  parameters = { :type => "Keyboard" }
235
278
  request = request("query", parameters)
236
- client = client(options)
279
+ client = http_client(options)
237
280
  response = client.post(request)
238
- hash = expect_200_response(response)
281
+ hash = expect_300_response(response)
239
282
  result = hash["result"]
240
283
  result.count != 0
241
284
  end
@@ -253,22 +296,135 @@ $ xcrun security find-identity -v -p codesigning
253
296
  }
254
297
  }
255
298
  request = request("gesture", parameters)
256
- client = client(options)
299
+ client = http_client(options)
257
300
  response = client.post(request)
258
- expect_200_response(response)
301
+ expect_300_response(response)
259
302
  end
260
303
 
261
304
  # @!visibility private
262
- def query(mark, options={})
263
- default_options = {
264
- all: false,
265
- specifier: :id
266
- }
267
- merged_options = default_options.merge(options)
305
+ #
306
+ # @example
307
+ # query({id: "login", :type "Button"})
308
+ #
309
+ # query({marked: "login"})
310
+ #
311
+ # query({marked: "login", type: "TextField"})
312
+ #
313
+ # query({type: "Button", index: 2})
314
+ #
315
+ # query({text: "Log in"})
316
+ #
317
+ # query({id: "hidden button", :all => true})
318
+ #
319
+ # # Escaping single quote is not necessary, but supported.
320
+ # query({text: "Karl's problem"})
321
+ # query({text: "Karl\'s problem"})
322
+ #
323
+ # # Escaping double quote is not necessary, but supported.
324
+ # query({text: "\"To know is not enough.\""})
325
+ # query({text: %Q["To know is not enough."]})
326
+ #
327
+ # Querying for text with newlines is not supported yet.
328
+ #
329
+ # The query language supports the following keys:
330
+ # * :marked - accessibilityIdentifier, accessibilityLabel, text, and value
331
+ # * :id - accessibilityIdentifier
332
+ # * :type - an XCUIElementType shorthand, e.g. XCUIElementTypeButton =>
333
+ # Button. See the link below for available types. Note, however that
334
+ # some XCUIElementTypes are not available on iOS.
335
+ # * :index - Applied after all other specifiers.
336
+ # * :all - Filter the result by visibility. Defaults to false. See the
337
+ # discussion below about visibility.
338
+ #
339
+ # ### Visibility
340
+ #
341
+ # The rules for visibility are:
342
+ #
343
+ # 1. If any part of the view is visible, the visible.
344
+ # 2. If the view has alpha 0, it is not visible.
345
+ # 3. If the view has a size (0,0) it is not visible.
346
+ # 4. If the view is not within the bounds of the screen, it is not visible.
347
+ #
348
+ # Visibility is determined using the "hitable" XCUIElement property.
349
+ # XCUITest, particularly under Xcode 7, is not consistent about setting
350
+ # the "hitable" property correctly. Views that are not "hitable" might
351
+ # respond to gestures.
352
+ #
353
+ # Regarding rule #1 - this is different from the Calabash iOS and Android
354
+ # definition of visibility which requires the mid-point of the view to be
355
+ # visible.
356
+ #
357
+ # ### Results
358
+ #
359
+ # Results are returned as an Array of Hashes.
360
+ #
361
+ # ```
362
+ # [
363
+ # {
364
+ # "enabled": true,
365
+ # "id": "mostly hidden button",
366
+ # "hitable": true,
367
+ # "rect": {
368
+ # "y": 459,
369
+ # "x": 24,
370
+ # "height": 25,
371
+ # "width": 100
372
+ # },
373
+ # "label": "Mostly Hidden",
374
+ # "type": "Button",
375
+ # "hit_point": {
376
+ # "x": 25,
377
+ # "y": 460
378
+ # },
379
+ # "test_id": 1
380
+ # }
381
+ # ]
382
+ # ```
383
+ #
384
+ # @see http://masilotti.com/xctest-documentation/Constants/XCUIElementType.html
385
+ # @param [Hash] uiquery A hash describing the query.
386
+ # @return [Array<Hash>] An array of elements matching the `uiquery`.
387
+ def query(uiquery)
388
+ merged_options = {
389
+ all: false
390
+ }.merge(uiquery)
391
+
392
+ allowed_keys = [:all, :id, :index, :marked, :text, :type]
393
+ unknown_keys = uiquery.keys - allowed_keys
394
+ if !unknown_keys.empty?
395
+ keys = allowed_keys.map { |key| ":#{key}" }.join(", ")
396
+ raise ArgumentError, %Q[
397
+ Unsupported key or keys found: '#{unknown_keys}'.
398
+
399
+ Allowed keys for a query are: #{keys}
400
+
401
+ ]
402
+ end
403
+
404
+ has_any_key = (allowed_keys & uiquery.keys).any?
405
+ if !has_any_key
406
+ keys = allowed_keys.map { |key| ":#{key}" }.join(", ")
407
+ raise ArgumentError, %Q[
408
+ Query does not contain any keysUnsupported key or keys found: '#{unknown_keys}'.
409
+
410
+ Allowed keys for a query are: #{keys}
411
+
412
+ ]
413
+ end
414
+
415
+ parameters = merged_options.dup.tap { |hs| hs.delete(:all) }
416
+ if parameters.empty?
417
+ keys = allowed_keys.map { |key| ":#{key}" }.join(", ")
418
+ raise ArgumentError, %Q[
419
+ Query must contain at least one of these keys:
420
+
421
+ #{keys}
422
+
423
+ ]
424
+ end
268
425
 
269
- parameters = { merged_options[:specifier] => mark }
270
426
  request = request("query", parameters)
271
- client = client(http_options)
427
+ client = http_client(http_options)
272
428
 
273
429
  RunLoop.log_debug %Q[Sending query with parameters:
274
430
 
@@ -277,7 +433,7 @@ $ xcrun security find-identity -v -p codesigning
277
433
  ]
278
434
 
279
435
  response = client.post(request)
280
- hash = expect_200_response(response)
436
+ hash = expect_300_response(response)
281
437
  elements = hash["result"]
282
438
 
283
439
  if merged_options[:all]
@@ -290,47 +446,99 @@ $ xcrun security find-identity -v -p codesigning
290
446
  end
291
447
 
292
448
  # @!visibility private
293
- def alert_visible?
449
+ def alert
294
450
  parameters = { :type => "Alert" }
295
451
  request = request("query", parameters)
296
- client = client(http_options)
452
+ client = http_client(http_options)
297
453
  response = client.post(request)
298
- hash = expect_200_response(response)
299
- !hash["result"].empty?
454
+ hash = expect_300_response(response)
455
+ hash["result"]
300
456
  end
301
457
 
302
458
  # @!visibility private
303
- def query_for_coordinate(mark)
304
- elements = query(mark)
305
- coordinate_from_query_result(elements)
459
+ def alert_visible?
460
+ !alert.empty?
306
461
  end
307
462
 
308
463
  # @!visibility private
309
- def touch(mark, options={})
310
- coordinate = query_for_coordinate(mark)
311
- perform_coordinate_gesture("touch",
312
- coordinate[:x], coordinate[:y],
313
- options)
464
+ def spring_board_alert
465
+ request = request("springBoardAlert")
466
+ client = http_client(http_options)
467
+ response = client.get(request)
468
+ hash = expect_300_response(response)
469
+ hash["result"]
314
470
  end
315
471
 
316
- alias_method :tap, :touch
472
+ # @!visibility private
473
+ def spring_board_alert_visible?
474
+ !spring_board_alert.empty?
475
+ end
317
476
 
318
477
  # @!visibility private
319
- def double_tap(mark, options={})
320
- coordinate = query_for_coordinate(mark)
478
+ # @see #query
479
+ def query_for_coordinate(uiquery)
480
+ element = wait_for_view(uiquery)
481
+ coordinate_from_query_result([element])
482
+ end
483
+
484
+ # @!visibility private
485
+ #
486
+ # :num_fingers
487
+ # :duration
488
+ # :repetitions
489
+ # @see #query
490
+ def touch(uiquery, options={})
491
+ coordinate = query_for_coordinate(uiquery)
492
+ perform_coordinate_gesture("touch", coordinate[:x], coordinate[:y], options)
493
+ end
494
+
495
+ # @!visibility private
496
+ # @see #touch
497
+ def touch_coordinate(coordinate, options={})
498
+ x = coordinate[:x] || coordinate["x"]
499
+ y = coordinate[:y] || coordinate["y"]
500
+ touch_point(x, y, options)
501
+ end
502
+
503
+ # @!visibility private
504
+ # @see #touch
505
+ def touch_point(x, y, options={})
506
+ perform_coordinate_gesture("touch", x, y, options)
507
+ end
508
+
509
+ # @!visibility private
510
+ # @see #touch
511
+ # @see #query
512
+ def double_tap(uiquery, options={})
513
+ coordinate = query_for_coordinate(uiquery)
321
514
  perform_coordinate_gesture("double_tap",
322
515
  coordinate[:x], coordinate[:y],
323
516
  options)
324
517
  end
325
518
 
326
519
  # @!visibility private
327
- def two_finger_tap(mark, options={})
328
- coordinate = query_for_coordinate(mark)
520
+ # @see #touch
521
+ # @see #query
522
+ def two_finger_tap(uiquery, options={})
523
+ coordinate = query_for_coordinate(uiquery)
329
524
  perform_coordinate_gesture("two_finger_tap",
330
525
  coordinate[:x], coordinate[:y],
331
526
  options)
332
527
  end
333
528
 
529
+ # @!visibility private
530
+ # @see #touch
531
+ # @see #query
532
+ def long_press(uiquery, options={})
533
+ merged_options = {
534
+ :duration => 1.1
535
+ }.merge(options)
536
+
537
+ coordinate = query_for_coordinate(uiquery)
538
+ perform_coordinate_gesture("touch", coordinate[:x], coordinate[:y],
539
+ merged_options)
540
+ end
541
+
334
542
  # @!visibility private
335
543
  def rotate_home_button_to(position, sleep_for=1.0)
336
544
  orientation = normalize_orientation_position(position)
@@ -338,9 +546,9 @@ $ xcrun security find-identity -v -p codesigning
338
546
  :orientation => orientation
339
547
  }
340
548
  request = request("rotate_home_button_to", parameters)
341
- client = client(http_options)
549
+ client = http_client(http_options)
342
550
  response = client.post(request)
343
- json = expect_200_response(response)
551
+ json = expect_300_response(response)
344
552
  sleep(sleep_for)
345
553
  json
346
554
  end
@@ -387,9 +595,9 @@ $ xcrun security find-identity -v -p codesigning
387
595
 
388
596
  ]
389
597
  request = request("gesture", parameters)
390
- client = client(http_options)
598
+ client = http_client(http_options)
391
599
  response = client.post(request)
392
- expect_200_response(response)
600
+ expect_300_response(response)
393
601
  end
394
602
 
395
603
  # @!visibility private
@@ -416,12 +624,11 @@ $ xcrun security find-identity -v -p codesigning
416
624
 
417
625
  #{JSON.pretty_generate(new_rect)}
418
626
 
419
- ])
627
+ ])
420
628
  {:x => touchx,
421
629
  :y => touchy}
422
630
  end
423
631
 
424
-
425
632
  # @!visibility private
426
633
  def change_volume(up_or_down)
427
634
  string = up_or_down.to_s
@@ -429,16 +636,217 @@ $ xcrun security find-identity -v -p codesigning
429
636
  :volume => string
430
637
  }
431
638
  request = request("volume", parameters)
432
- client = client(http_options)
639
+ client = http_client(http_options)
433
640
  response = client.post(request)
434
- json = expect_200_response(response)
641
+ json = expect_300_response(response)
435
642
  # Set in the route
436
643
  sleep(0.2)
437
644
  json
438
645
  end
439
646
 
647
+ # TODO: animation model
648
+ def wait_for_animations
649
+ sleep(0.5)
650
+ end
651
+
652
+ # @!visibility private
653
+ def wait_for(timeout_message, options={}, &block)
654
+ wait_options = WAIT_DEFAULTS.merge(options)
655
+ timeout = wait_options[:timeout]
656
+ exception_class = wait_options[:exception_class]
657
+ with_timeout(timeout, timeout_message, exception_class) do
658
+ loop do
659
+ value = block.call
660
+ return value if value
661
+ sleep(wait_options[:retry_frequency])
662
+ end
663
+ end
664
+ end
665
+
666
+ # @!visibility private
667
+ def wait_for_keyboard(timeout=WAIT_DEFAULTS[:timeout])
668
+ options = WAIT_DEFAULTS.dup
669
+ options[:timeout] = timeout
670
+ message = %Q[
671
+
672
+ Timed out after #{timeout} seconds waiting for the keyboard to appear.
673
+
674
+ ]
675
+ wait_for(message, options) do
676
+ keyboard_visible?
677
+ end
678
+ end
679
+
680
+ # @!visibility private
681
+ def wait_for_alert(timeout=WAIT_DEFAULTS[:timeout])
682
+ options = WAIT_DEFAULTS.dup
683
+ options[:timeout] = timeout
684
+ message = %Q[
685
+
686
+ Timed out after #{timeout} seconds waiting for an alert to appear.
687
+
688
+ ]
689
+ wait_for(message, options) do
690
+ alert_visible?
691
+ end
692
+ end
693
+
694
+ # @!visibility private
695
+ def wait_for_no_alert(timeout=WAIT_DEFAULTS[:timeout])
696
+ options = WAIT_DEFAULTS.dup
697
+ options[:timeout] = timeout
698
+ message = %Q[
699
+
700
+ Timed out after #{timeout} seconds waiting for an alert to disappear.
701
+
702
+ ]
703
+
704
+ wait_for(message, options) do
705
+ !alert_visible?
706
+ end
707
+ end
708
+
709
+ # @!visibility private
710
+ def wait_for_text_in_view(text, uiquery, options={})
711
+ merged_options = WAIT_DEFAULTS.merge(options)
712
+ result = wait_for_view(uiquery, merged_options)
713
+
714
+ candidates = [result["value"],
715
+ result["label"]]
716
+ match = candidates.any? do |elm|
717
+ elm == text
718
+ end
719
+ if !match
720
+ fail(%Q[
721
+
722
+ Expected to find '#{text}' as a 'value' or 'label' in
723
+
724
+ #{JSON.pretty_generate(result)}
725
+
726
+ ])
727
+ end
728
+ end
729
+
730
+ # @!visibility private
731
+ def wait_for_view(uiquery, options={})
732
+ merged_options = WAIT_DEFAULTS.merge(options)
733
+
734
+ unless merged_options[:message]
735
+ message = %Q[
736
+
737
+ Waited #{merged_options[:timeout]} seconds for
738
+
739
+ #{uiquery}
740
+
741
+ to match a view.
742
+
743
+ ]
744
+ merged_options[:timeout_message] = message
745
+ end
746
+
747
+ result = nil
748
+ wait_for(merged_options[:timeout_message], options) do
749
+ result = query(uiquery)
750
+ !result.empty?
751
+ end
752
+
753
+ result[0]
754
+ end
755
+
756
+ # @!visibility private
757
+ def wait_for_no_view(uiquery, options={})
758
+ merged_options = WAIT_DEFAULTS.merge(options)
759
+ unless merged_options[:message]
760
+ message = %Q[
761
+
762
+ Waited #{merged_options[:timeout]} seconds for
763
+
764
+ #{uiquery}
765
+
766
+ to match no views.
767
+
768
+ ]
769
+ merged_options[:timeout_message] = message
770
+ end
771
+
772
+ result = nil
773
+ wait_for(merged_options[:timeout_message], options) do
774
+ result = query(uiquery)
775
+ result.empty?
776
+ end
777
+
778
+ result[0]
779
+ end
780
+
781
+ # @!visibility private
782
+ class PrivateWaitTimeoutError < RuntimeError ; end
783
+
784
+ # @!visibility private
785
+ def with_timeout(timeout, timeout_message,
786
+ exception_class=WAIT_DEFAULTS[:exception_class], &block)
787
+ if timeout_message.nil? ||
788
+ (timeout_message.is_a?(String) && timeout_message.empty?)
789
+ raise ArgumentError, 'You must provide a timeout message'
790
+ end
791
+
792
+ unless block_given?
793
+ raise ArgumentError, 'You must provide a block'
794
+ end
795
+
796
+ # Timeout.timeout will never timeout if the given `timeout` is zero.
797
+ # We will raise an exception if the timeout is zero.
798
+ # Timeout.timeout already raises an exception if `timeout` is negative.
799
+ if timeout == 0
800
+ raise ArgumentError, 'Timeout cannot be 0'
801
+ end
802
+
803
+ message = if timeout_message.is_a?(Proc)
804
+ timeout_message.call({timeout: timeout})
805
+ else
806
+ timeout_message
807
+ end
808
+
809
+ failed = false
810
+
811
+ begin
812
+ Timeout.timeout(timeout, PrivateWaitTimeoutError) do
813
+ return block.call
814
+ end
815
+ rescue PrivateWaitTimeoutError => _
816
+ # If we raise Timeout here the stack trace will be cluttered and we
817
+ # wish to show the user a clear message, avoiding
818
+ # "`rescue in with_timeout':" in the stack trace.
819
+ failed = true
820
+ end
821
+
822
+ if failed
823
+ fail(exception_class, message)
824
+ end
825
+ end
826
+
827
+ # @!visibility private
828
+ def fail(*several_variants)
829
+ arg0 = several_variants[0]
830
+ arg1 = several_variants[1]
831
+
832
+ if arg1.nil?
833
+ exception_type = RuntimeError
834
+ message = arg0
835
+ else
836
+ exception_type = arg0
837
+ message = arg1
838
+ end
839
+
840
+ raise exception_type, message
841
+ end
842
+
843
+ =begin
844
+ PRIVATE
845
+ =end
440
846
  private
441
847
 
848
+ attr_reader :http_client
849
+
442
850
  # @!visibility private
443
851
  def xcrun
444
852
  RunLoop::Xcrun.new
@@ -511,8 +919,28 @@ $ xcrun security find-identity -v -p codesigning
511
919
  end
512
920
 
513
921
  # @!visibility private
514
- def client(options={})
515
- RunLoop::HTTP::RetriableClient.new(server, options)
922
+ def http_client(options={})
923
+ if !@http_client
924
+ @http_client = RunLoop::HTTP::RetriableClient.new(server, options)
925
+ else
926
+ # If the options are different, create a new client
927
+ if options[:retries] != @http_client.retries ||
928
+ options[:timeout] != @http_client.timeout ||
929
+ options[:interval] != @http_client.interval
930
+ reset_http_client!
931
+ @http_client = RunLoop::HTTP::RetriableClient.new(server, options)
932
+ else
933
+ end
934
+ end
935
+ @http_client
936
+ end
937
+
938
+ # @!visibility private
939
+ def reset_http_client!
940
+ if @http_client
941
+ @http_client.reset_all!
942
+ @http_client = nil
943
+ end
516
944
  end
517
945
 
518
946
  # @!visibility private
@@ -549,87 +977,108 @@ $ xcrun security find-identity -v -p codesigning
549
977
  end
550
978
  end
551
979
 
980
+ # @!visibility private
981
+ def server_pid
982
+ options = http_options
983
+ request = request("pid")
984
+ client = http_client(options)
985
+ response = client.get(request)
986
+ expect_300_response(response)
987
+ end
988
+
989
+ # @!visibility private
990
+ def session_identifier
991
+ options = http_options
992
+ request = request("sessionIdentifier")
993
+ client = http_client(options)
994
+ response = client.get(request)
995
+ expect_300_response(response)
996
+ end
997
+
552
998
  # @!visibility private
553
999
  def session_delete
554
1000
  # https://xamarin.atlassian.net/browse/TCFW-255
555
1001
  # httpclient is unable to send a valid DELETE
556
1002
  args = ["curl", "-X", "DELETE", %Q[#{url}#{versioned_route("session")}]]
557
- run_shell_command(args, {:log_cmd => true})
558
-
559
- # options = ping_options
560
- # request = request("session")
561
- # client = client(options)
562
- # begin
563
- # response = client.delete(request)
564
- # body = expect_200_response(response)
565
- # RunLoop.log_debug("CBX-Runner says, #{body}")
566
- # body
567
- # rescue => e
568
- # RunLoop.log_debug("CBX-Runner session delete error: #{e}")
569
- # nil
570
- # end
1003
+
1004
+ begin
1005
+ run_shell_command(args, {:log_cmd => true, :timeout => 10})
1006
+ rescue Shell::TimeoutError => _
1007
+ RunLoop.log_debug("Timed out calling DELETE session/ after 10 seconds")
1008
+ end
571
1009
  end
572
1010
 
573
1011
  # @!visibility private
574
- # TODO expect 200 response and parse body (atm the body in not valid JSON)
575
1012
  def shutdown
576
- session_delete
577
- options = ping_options
578
- request = request("shutdown")
579
- client = client(options)
580
- body = nil
1013
+
1014
+ if RunLoop::Environment.xtc?
1015
+ RunLoop.log_error("Calling shutdown on the XTC is not supported.")
1016
+ return
1017
+ end
1018
+
581
1019
  begin
582
- response = client.post(request)
583
- body = response.body
584
- RunLoop.log_debug("DeviceAgent-Runner says, \"#{body}\"")
585
-
586
- now = Time.now
587
- poll_until = now + 10.0
588
- running = true
589
- while Time.now < poll_until
590
- running = !running?
591
- break if running
592
- sleep(0.1)
593
- end
1020
+ if !running?
1021
+ RunLoop.log_debug("DeviceAgent-Runner is not running")
1022
+ else
1023
+ session_delete
1024
+
1025
+ request = request("shutdown")
1026
+ client = http_client(ping_options)
1027
+ response = client.post(request)
1028
+ hash = expect_300_response(response)
1029
+ message = hash["message"]
1030
+
1031
+ RunLoop.log_debug(%Q[DeviceAgent-Runner says, "#{message}"])
1032
+
1033
+ now = Time.now
1034
+ poll_until = now + 10.0
1035
+ stopped = false
1036
+ while Time.now < poll_until
1037
+ stopped = !running?
1038
+ break if stopped
1039
+ sleep(0.1)
1040
+ end
594
1041
 
595
- RunLoop.log_debug("Waited for #{Time.now - now} seconds for DeviceAgent to shutdown")
596
- body
597
- rescue => e
598
- RunLoop.log_debug("DeviceAgent-Runner shutdown error: #{e}")
1042
+ RunLoop.log_debug("Waited for #{Time.now - now} seconds for DeviceAgent to shutdown")
1043
+ end
1044
+ rescue RunLoop::DeviceAgent::Client::HTTPError => e
1045
+ RunLoop.log_debug("DeviceAgent-Runner shutdown error: #{e.message}")
599
1046
  ensure
600
- quit_options = { :timeout => 0.5 }
601
- term_options = { :timeout => 0.5 }
602
- kill_options = { :timeout => 0.5 }
603
-
604
- process_name = "iOSDeviceManager"
605
- RunLoop::ProcessWaiter.new(process_name).pids.each do |pid|
606
- quit = RunLoop::ProcessTerminator.new(pid, "QUIT", process_name, quit_options)
607
- if !quit.kill_process
608
- term = RunLoop::ProcessTerminator.new(pid, "TERM", process_name, term_options)
609
- if !term.kill_process
610
- kill = RunLoop::ProcessTerminator.new(pid, "KILL", process_name, kill_options)
611
- kill.kill_process
612
- end
1047
+ if @launcher_pid
1048
+ term_options = { :timeout => 1.5 }
1049
+ kill_options = { :timeout => 1.0 }
1050
+
1051
+ process_name = cbx_launcher.name
1052
+ pid = @launcher_pid.to_i
1053
+
1054
+ term = RunLoop::ProcessTerminator.new(pid, "TERM", process_name, term_options)
1055
+ if !term.kill_process
1056
+ kill = RunLoop::ProcessTerminator.new(pid, "KILL", process_name, kill_options)
1057
+ kill.kill_process
1058
+ end
1059
+
1060
+ if process_name == :xcodebuild
1061
+ sleep(10)
613
1062
  end
614
1063
  end
615
1064
  end
616
- body
1065
+ hash
617
1066
  end
618
1067
 
619
1068
  # @!visibility private
620
- # TODO expect 200 response and parse body (atm the body is not valid JSON)
621
1069
  def health(options={})
622
1070
  merged_options = http_options.merge(options)
623
1071
  request = request("health")
624
- client = client(merged_options)
1072
+ client = http_client(merged_options)
625
1073
  response = client.get(request)
626
- body = response.body
627
- RunLoop.log_debug("CBX-Runner driver says, \"#{body}\"")
628
- body
1074
+ hash = expect_300_response(response)
1075
+ status = hash["status"]
1076
+ RunLoop.log_debug(%Q[DeviceAgent says, "#{status}"])
1077
+ hash
629
1078
  end
630
1079
 
631
-
632
- # TODO cbx_runner_stale? returns false always
1080
+ # TODO Might not be necessary - this is an edge case and it is likely
1081
+ # that iOSDeviceManager will be able to handle this for us.
633
1082
  def cbx_runner_stale?
634
1083
  false
635
1084
  # The RunLoop::Version class needs to be updated to handle timestamps.
@@ -646,58 +1095,35 @@ $ xcrun security find-identity -v -p codesigning
646
1095
  end
647
1096
 
648
1097
  # @!visibility private
649
- def launch_cbx_runner(options={})
650
- merged_options = DEFAULTS.merge(options)
1098
+ def launch_cbx_runner
1099
+ options = launcher_options
651
1100
 
652
- if merged_options[:shutdown_device_agent_before_launch]
1101
+ if options[:shutdown_device_agent_before_launch]
653
1102
  RunLoop.log_debug("Launch options insist that the DeviceAgent be shutdown")
654
1103
  shutdown
1104
+ end
655
1105
 
656
- if cbx_launcher.name == :xcodebuild
657
- sleep(5.0)
658
- end
1106
+ if cbx_runner_stale?
1107
+ RunLoop.log_debug("The DeviceAgent that is running is stale; shutting down")
1108
+ shutdown
659
1109
  end
660
1110
 
661
1111
  if running?
662
1112
  RunLoop.log_debug("DeviceAgent is already running")
663
- if cbx_runner_stale?
664
- shutdown
665
- else
666
- # TODO: is it necessary to return the pid? Or can we return true?
667
- return server_pid
668
- end
669
- end
670
-
671
- if cbx_launcher.name == :xcodebuild
672
- RunLoop.log_debug("xcodebuild is the launcher - terminating existing xcodebuild processes")
673
- term_options = { :timeout => 0.5 }
674
- kill_options = { :timeout => 0.5 }
675
- RunLoop::ProcessWaiter.new("xcodebuild").pids.each do |pid|
676
- term = RunLoop::ProcessTerminator.new(pid, 'TERM', "xcodebuild", term_options)
677
- killed = term.kill_process
678
- unless killed
679
- RunLoop::ProcessTerminator.new(pid, 'KILL', "xcodebuild", kill_options)
680
- end
681
- end
682
- sleep(2.0)
1113
+ return true
683
1114
  end
684
1115
 
685
1116
  start = Time.now
686
- RunLoop.log_debug("Waiting for CBX-Runner to launch...")
687
- pid = cbx_launcher.launch(options)
688
-
689
- if cbx_launcher.name == :xcodebuild
690
- sleep(2.0)
691
- end
1117
+ RunLoop.log_debug("Waiting for DeviceAgent to launch...")
1118
+ @launcher_pid = cbx_launcher.launch(options)
692
1119
 
693
1120
  begin
694
- timeout = RunLoop::Environment.ci? ? 120 : 60
1121
+ timeout = options[:device_agent_install_timeout] * 1.5
695
1122
  health_options = {
696
1123
  :timeout => timeout,
697
1124
  :interval => 0.1,
698
1125
  :retries => (timeout/0.1).to_i
699
1126
  }
700
-
701
1127
  health(health_options)
702
1128
  rescue RunLoop::HTTP::Error => _
703
1129
  raise %Q[
@@ -715,16 +1141,15 @@ $ tail -1000 -F #{cbx_launcher.class.log_file}
715
1141
  end
716
1142
 
717
1143
  RunLoop.log_debug("Took #{Time.now - start} launch and respond to /health")
718
-
719
- # TODO: is it necessary to return the pid? Or can we return true?
720
- pid
1144
+ true
721
1145
  end
722
1146
 
723
1147
  # @!visibility private
724
1148
  def launch_aut(bundle_id = @bundle_id)
725
- client = client(http_options)
1149
+ client = http_client(http_options)
726
1150
  request = request("session", {:bundleID => bundle_id})
727
1151
 
1152
+ # This check needs to be done _before_ the DeviceAgent is launched.
728
1153
  if device.simulator?
729
1154
  # Yes, we could use iOSDeviceManager to check, I dont understand the
730
1155
  # behavior yet - does it require the simulator be launched?
@@ -736,7 +1161,10 @@ $ tail -1000 -F #{cbx_launcher.class.log_file}
736
1161
  RunLoop.log_debug("Detected :xcodebuild launcher; skipping app installed check")
737
1162
  installed = true
738
1163
  else
739
- installed = cbx_launcher.app_installed?(bundle_id)
1164
+ # Too slow for most devices
1165
+ # https://jira.xamarin.com/browse/TCFW-273
1166
+ # installed = cbx_launcher.app_installed?(bundle_id)
1167
+ installed = true
740
1168
  end
741
1169
  end
742
1170
 
@@ -752,17 +1180,28 @@ Please install it.
752
1180
  ]
753
1181
  end
754
1182
 
1183
+ retries = 5
1184
+
755
1185
  begin
756
1186
  response = client.post(request)
757
1187
  RunLoop.log_debug("Launched #{bundle_id} on #{device}")
758
1188
  RunLoop.log_debug("#{response.body}")
759
- if device.simulator?
760
- # It is not clear yet whether we should do this. There is a problem
761
- # in the simulator_wait_for_stable_state; it waits too long.
762
- # device.simulator_wait_for_stable_state
763
- end
764
- expect_200_response(response)
1189
+
1190
+ expect_300_response(response)
765
1191
  rescue => e
1192
+ retries = retries - 1
1193
+ if !RunLoop::Environment.xtc?
1194
+ if retries >= 0
1195
+ if !running?
1196
+ RunLoop.log_debug("The DeviceAgent stopped running after POST /session; retrying")
1197
+ launch_cbx_runner
1198
+ else
1199
+ RunLoop.log_debug("Failed to launch the AUT: #{bundle_id}; retrying")
1200
+ end
1201
+ retry
1202
+ end
1203
+ end
1204
+
766
1205
  raise e.class, %Q[
767
1206
 
768
1207
  Could not launch #{bundle_id} on #{device}:
@@ -781,35 +1220,44 @@ Something went wrong.
781
1220
  begin
782
1221
  JSON.parse(body)
783
1222
  rescue TypeError, JSON::ParserError => _
784
- raise RunLoop::DeviceAgent::Client::HTTPError,
785
- "Could not parse response '#{body}'; the app has probably crashed"
1223
+ raise RunLoop::DeviceAgent::Client::HTTPError, %Q[
1224
+ Could not parse response from server:
1225
+
1226
+ body => "#{body}"
1227
+
1228
+ If the body empty, the DeviceAgent has probably crashed.
1229
+
1230
+ ]
786
1231
  end
787
1232
  end
788
1233
 
789
1234
  # @!visibility private
790
- def expect_200_response(response)
1235
+ def expect_300_response(response)
791
1236
  body = response_body_to_hash(response)
792
- if response.status_code < 300 && !body["error"]
1237
+ if response.status_code < 400 && !body["error"]
793
1238
  return body
794
1239
  end
795
1240
 
796
- if response.status_code > 300
1241
+ reset_http_client!
1242
+
1243
+ if response.status_code >= 400
797
1244
  raise RunLoop::DeviceAgent::Client::HTTPError,
798
- %Q[Expected status code < 300, found #{response.status_code}.
1245
+ %Q[
1246
+ Expected status code < 400, found #{response.status_code}.
799
1247
 
800
1248
  Server replied with:
801
1249
 
802
1250
  #{body}
803
1251
 
804
- ]
1252
+ ]
805
1253
  else
806
1254
  raise RunLoop::DeviceAgent::Client::HTTPError,
807
- %Q[Expected JSON response with no error, but found
1255
+ %Q[
1256
+ Expected JSON response with no error, but found
808
1257
 
809
1258
  #{body["error"]}
810
1259
 
811
- ]
812
-
1260
+ ]
813
1261
  end
814
1262
  end
815
1263