run_loop 2.1.9 → 2.1.10

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
  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