run_loop 2.2.4 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,7 +31,7 @@ module RunLoop
31
31
  # exceeded - if the default 30 seconds has passed, the
32
32
  # simulator is probably stable enough for subsequent
33
33
  # operations.
34
- :timeout => RunLoop::Environment.ci? ? 120 : 30
34
+ :timeout => RunLoop::Environment.ci? ? 240 : 120
35
35
  }
36
36
 
37
37
  attr_reader :name
@@ -322,124 +322,54 @@ version: #{version}
322
322
  end
323
323
 
324
324
  # @!visibility private
325
- #
326
- # Waits for three conditions:
327
- #
328
- # 1. The SHA sum of the simulator data/ directory to be stable.
329
- # 2. No more log messages are begin generated.
330
- # 3. 1 and 2 must hold for 1.5 seconds.
331
- #
332
- # When the simulator version is >= iOS 9, two more conditions are added to
333
- # get past the iOS 9+ boot screen.
334
- #
335
- # 4. Wait for com.apple.audio.SystemSoundServer-iOS-Simulator process to
336
- # start.
337
- # 5. 1 and 2 must hold for 1.5 seconds.
338
- #
339
- # When the simulator version is >= iOS 9 and the device is an iPad another
340
- # condition is added because simctl fails to correctly install applications;
341
- # the app and data container exists, but Springboard does not detect them.
342
- #
343
- # 6. 1 and 2 must hold for 1.5 seconds.
344
- #
345
- # TODO needs update for Xcode 8 + iOS 10 simulators.
346
- def simulator_wait_for_stable_state
347
-
348
- # How long to wait between stability checks.
349
- # Shorter than this gives false positives.
350
- delay = 0.5
351
-
352
- # How many times to wait for stable state.
353
- max_stable_count = 3
354
-
355
- # How long to wait for iOS 9 boot screen.
356
- boot_screen_wait_options = {
357
- :max_boot_screen_wait => 10,
358
- :raise_on_timeout => false
359
- }
360
-
361
- # How much additional time to wait for iOS 9+ iPads.
362
- #
363
- # Installing and launching on iPads is problematic.
364
- # Sometimes the app is installed, but SpringBoard does
365
- # not recognize that the app is installed even though
366
- # simctl says that it is.
367
- additional_ipad_delay = delay * 2
368
-
369
- # Adjust for CI environments
370
- if RunLoop::Environment.ci?
371
- max_stable_count = 5
372
- boot_screen_wait_options[:max_boot_screen_wait] = 20
373
- additional_ipad_delay = delay * 4
374
- end
375
-
376
- # iOS 9 simulators have an additional boot screen.
377
- is_gte_ios9 = version >= RunLoop::Version.new('9.0')
378
-
379
- # Xcode 8 simulators do not need to wait for log file
380
- is_xcode8 = RunLoop::Xcode.new.version_gte_8?
325
+ # In megabytes
326
+ def simulator_size_on_disk
327
+ data_path = File.join(simulator_root_dir, 'data')
328
+ RunLoop::Directory.size(data_path, :mb)
329
+ end
381
330
 
382
- # iOS > 9 iPad simulators need additional time to stabilize, especially
383
- # to ensure that `simctl install` notifies SpringBoard that a new app
384
- # has been installed.
385
- is_ipad = simulator_is_ipad?
331
+ # @!visibility private
332
+ def simulator_wait_for_stable_state
333
+ required = simulator_required_child_processes
386
334
 
387
335
  timeout = SIM_STABLE_STATE_OPTIONS[:timeout]
388
336
  now = Time.now
389
337
  poll_until = now + timeout
390
338
 
391
339
  RunLoop.log_debug("Waiting for simulator to stabilize with timeout: #{timeout} seconds")
340
+ footprint = simulator_size_on_disk
392
341
 
393
- current_dir_sha = simulator_data_directory_sha
394
- if is_xcode8
395
- current_log_sha = true
396
- else
397
- current_log_sha = simulator_log_file_sha
398
- end
399
-
400
- is_stable = false
401
- waited_for_boot = false
402
- waited_for_ipad = false
403
- stable_count = 0
404
-
405
- while Time.now < poll_until do
406
- latest_dir_sha = simulator_data_directory_sha
407
- if is_xcode8
408
- latest_log_sha = true
342
+ if version.major >= 9 && footprint < 18
343
+ first_launch = true
344
+ elsif version.major == 8
345
+ if version.minor >= 3 && footprint < 19
346
+ first_launch = true
409
347
  else
410
- latest_log_sha = simulator_log_file_sha
348
+ first_launch = footprint < 11
411
349
  end
350
+ else
351
+ first_launch = false
352
+ end
412
353
 
413
- is_stable = [current_dir_sha == latest_dir_sha,
414
- current_log_sha == latest_log_sha].all?
415
-
416
- if is_stable
417
- stable_count = stable_count + 1
418
- if stable_count == max_stable_count
419
- if is_gte_ios9 && !waited_for_boot
420
- process_name = "com.apple.audio.SystemSoundServer-iOS-Simulator"
421
- RunLoop::ProcessWaiter.new(process_name, boot_screen_wait_options).wait_for_any
422
- waited_for_boot = true
423
- stable_count = 0
424
- elsif is_gte_ios9 && is_ipad && !waited_for_ipad
425
- RunLoop.log_debug("Waiting additional time for iOS 9 iPad to stabilize")
426
- sleep(additional_ipad_delay)
427
- waited_for_ipad = true
428
- stable_count = 0
429
- else
430
- break
431
- end
354
+ while !required.empty? && Time.now < poll_until do
355
+ sleep(0.5)
356
+ required = required.map do |process_name|
357
+ if simulator_process_running?(process_name)
358
+ nil
359
+ else
360
+ process_name
432
361
  end
433
- end
434
-
435
- current_dir_sha = latest_dir_sha
436
- current_log_sha = latest_log_sha
437
- sleep(delay)
362
+ end.compact
438
363
  end
439
364
 
440
- if is_stable
365
+ if required.empty?
441
366
  elapsed = Time.now - now
442
- RunLoop.log_debug("Waited a total of #{elapsed} seconds for simulator to stabilize")
367
+ RunLoop.log_debug("All required simulator processes have started after #{elapsed}")
368
+ if first_launch
369
+ RunLoop.log_debug("Detected a first launch, waiting a little longer - footprint was #{footprint} MB")
370
+ sleep(RunLoop::Environment.ci? ? 10 : 5)
371
+ end
372
+ RunLoop.log_debug("Waited for #{elapsed} seconds for simulator to stabilize")
443
373
  else
444
374
  RunLoop.log_debug("Timed out after #{timeout} seconds waiting for simulator to stabilize")
445
375
  end
@@ -541,9 +471,61 @@ failed with this output:
541
471
  simulator_languages
542
472
  end
543
473
 
474
+ # @!visibility private
475
+ def simulator_running_app_details
476
+ pids = simulator_running_app_pids
477
+ running_apps = {}
478
+
479
+ pids.each do |pid|
480
+ cmd = ["ps", "-o", "comm=", "-p", pid.to_s]
481
+
482
+ hash = run_shell_command(cmd)
483
+ out = hash[:out]
484
+
485
+ if out.nil? || out == "" || out.strip.nil?
486
+ nil
487
+ else
488
+ name = out.strip.split("/").last
489
+
490
+ cmd = ["ps", "-o", "command=", "-p", pid.to_s]
491
+ hash = run_shell_command(cmd)
492
+ out = hash[:out]
493
+
494
+ if out.nil? || out == "" || out.strip.nil?
495
+ nil
496
+ else
497
+ tokens = out.split("#{name} ")
498
+
499
+ # No arguments
500
+ if tokens.count == 1
501
+ args = ""
502
+ else
503
+ args = tokens.last.strip
504
+ end
505
+
506
+ running_apps[name] = {
507
+ args: args,
508
+ command: out.strip
509
+ }
510
+ end
511
+ end
512
+ end
513
+
514
+ running_apps
515
+ end
516
+
517
+ =begin
518
+ PRIVATE METHODS
519
+ =end
520
+
544
521
  private
545
522
 
546
- attr_reader :pbuddy, :simctl, :xcrun
523
+ attr_reader :pbuddy, :simctl, :xcrun, :xcode
524
+
525
+ # @!visibility private
526
+ def xcode
527
+ @xcode ||= RunLoop::Xcode.new
528
+ end
547
529
 
548
530
  # @!visibility private
549
531
  def xcrun
@@ -610,6 +592,76 @@ failed with this output:
610
592
  udid
611
593
  end
612
594
 
595
+ # @!visibility private
596
+ def simulator_required_child_processes
597
+ @simulator_required_child_processes ||= begin
598
+ required = ["backboardd", "installd", "SimulatorBridge", "SpringBoard"]
599
+ if xcode.version_gte_8? && version.major > 8
600
+ required << "medialibraryd"
601
+ end
602
+
603
+ if simulator_is_ipad? && version.major == 9
604
+ required << "com.apple.audio.SystemSoundServer-iOS-Simulator"
605
+ end
606
+
607
+ required
608
+ end
609
+ end
610
+
611
+ # @!visibility private
612
+ def simulator_launchd_sim_pid
613
+ waiter = RunLoop::ProcessWaiter.new("launchd_sim")
614
+ waiter.wait_for_any
615
+
616
+ return nil if !waiter.running_process?
617
+
618
+ pid = nil
619
+
620
+ waiter.pids.each do |launchd_sim_pid|
621
+ cmd = ["ps", "x", "-o", "pid,command", launchd_sim_pid.to_s]
622
+ hash = run_shell_command(cmd)
623
+ out = hash[:out]
624
+ process_line = out.split($-0)[1]
625
+ if !process_line || process_line == ""
626
+ false
627
+ else
628
+ pid = process_line.split(" ").first.strip
629
+ if process_line[/#{udid}/] == nil
630
+ RunLoop.log_debug("Terminating launchd_sim process with pid #{pid}")
631
+ RunLoop::ProcessTerminator.new(pid, "KILL", "launchd_sim").kill_process
632
+ pid = nil
633
+ end
634
+ end
635
+ end
636
+ pid
637
+ end
638
+
639
+ # @!visibility private
640
+ def process_parent_is_launchd_sim?(pid)
641
+ launchd_sim_pid = simulator_launchd_sim_pid
642
+ return false if !launchd_sim_pid
643
+
644
+ cmd = ["ps", "x", "-o", "ppid=", "-p", pid.to_s]
645
+ hash = run_shell_command(cmd)
646
+
647
+ out = hash[:out]
648
+ if out.nil? || out == ""
649
+ false
650
+ else
651
+ ppid = out.strip
652
+ ppid == launchd_sim_pid.to_s
653
+ end
654
+ end
655
+
656
+ # @!visibility private
657
+ def simulator_process_running?(process_name)
658
+ waiter = RunLoop::ProcessWaiter.new(process_name)
659
+ waiter.pids.any? do |pid|
660
+ process_parent_is_launchd_sim?(pid)
661
+ #process_parent_is_current_xcode?(pid)
662
+ end
663
+ end
664
+
613
665
  # @!visibility private
614
666
  def simulator_data_directory_sha
615
667
  path = File.join(simulator_root_dir, 'data')
@@ -688,6 +740,26 @@ https://github.com/calabash/calabash-ios/wiki/Testing-on-Physical-Devices
688
740
  end
689
741
  true
690
742
  end
743
+
744
+ # @!visibility private
745
+ def simulator_running_app_pids
746
+ simulator_running_user_app_pids +
747
+ simulator_running_system_app_pids
748
+ end
749
+
750
+ # @!visibility private
751
+ def simulator_running_user_app_pids
752
+ path = File.join(udid, "data", "Containers", "Bundle")
753
+ RunLoop::ProcessWaiter.pgrep_f(path)
754
+ end
755
+
756
+ # @!visibility private
757
+ def simulator_running_system_app_pids
758
+ base_dir = xcode.developer_dir
759
+ sim_apps_dir = "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/Applications"
760
+ path = File.expand_path(File.join(base_dir, sim_apps_dir))
761
+ RunLoop::ProcessWaiter.pgrep_f(path)
762
+ end
691
763
  end
692
764
  end
693
765
 
@@ -38,12 +38,20 @@ module RunLoop
38
38
 
39
39
  # Ignored in the XTC.
40
40
  # This key is subject to removal or changes
41
- :device_agent_install_timeout => RunLoop::Environment.ci? ? 120 : 90,
41
+ :device_agent_install_timeout => RunLoop::Environment.ci? ? 240 : 120,
42
42
  # This value must always be false on the XTC.
43
43
  # This is should only be used by gem maintainers or very advanced users.
44
- :shutdown_device_agent_before_launch => false
44
+ :shutdown_device_agent_before_launch => false,
45
+
46
+ # This value was derived empirically by typing hundreds of strings
47
+ # using XCUIElement#typeText. It corresponds to the DeviceAgent
48
+ # constant CBX_DEFAULT_SEND_STRING_FREQUENCY which is 60. _Decrease_
49
+ # this value if you are timing out typing strings.
50
+ :characters_per_second => 12
45
51
  }
46
52
 
53
+ AUT_LAUNCHED_BY_RUN_LOOP_ARG = "LAUNCHED_BY_RUN_LOOP"
54
+
47
55
  # @!visibility private
48
56
  #
49
57
  # These defaults may change at any time.
@@ -67,7 +75,6 @@ module RunLoop
67
75
 
68
76
  # @!visibility private
69
77
  def self.run(options={})
70
- # logger = options[:logger]
71
78
  simctl = options[:sim_control] || options[:simctl] || RunLoop::Simctl.new
72
79
  xcode = options[:xcode] || RunLoop::Xcode.new
73
80
  instruments = options[:instruments] || RunLoop::Instruments.new
@@ -88,10 +95,19 @@ module RunLoop
88
95
  default_options = {
89
96
  :xcode => xcode
90
97
  }
98
+
91
99
  merged_options = default_options.merge(options)
92
100
 
93
101
  if device.simulator? && app
102
+ RunLoop::Core.expect_simulator_compatible_arch(device, app)
103
+
104
+ if merged_options[:relaunch_simulator]
105
+ RunLoop.log_debug("Detected :relaunch_simulator option; will force simulator to restart")
106
+ RunLoop::CoreSimulator.quit_simulator
107
+ end
108
+
94
109
  core_sim = RunLoop::CoreSimulator.new(device, app, merged_options)
110
+
95
111
  if reset_options
96
112
  core_sim.reset_app_sandbox
97
113
  end
@@ -100,7 +116,7 @@ module RunLoop
100
116
  core_sim.install
101
117
  end
102
118
 
103
- cbx_launcher = Client.detect_cbx_launcher(options, device)
119
+ cbx_launcher = Client.detect_cbx_launcher(merged_options, device)
104
120
 
105
121
  code_sign_identity = options[:code_sign_identity]
106
122
  if !code_sign_identity
@@ -114,6 +130,10 @@ module RunLoop
114
130
  aut_args = options.fetch(:args, [])
115
131
  aut_env = options.fetch(:env, {})
116
132
 
133
+ if !aut_args.include?(AUT_LAUNCHED_BY_RUN_LOOP_ARG)
134
+ aut_args << AUT_LAUNCHED_BY_RUN_LOOP_ARG
135
+ end
136
+
117
137
  launcher_options = {
118
138
  code_sign_identity: code_sign_identity,
119
139
  device_agent_install_timeout: install_timeout,
@@ -282,6 +302,9 @@ INSTANCE METHODS
282
302
  # @!visibility private
283
303
  #
284
304
  # Experimental!
305
+ #
306
+ # This will launch the other app using the same arguments and environment
307
+ # as the AUT.
285
308
  def launch_other_app(bundle_id)
286
309
  launch_aut(bundle_id)
287
310
  end
@@ -327,7 +350,9 @@ INSTANCE METHODS
327
350
 
328
351
  # @!visibility private
329
352
  def clear_text
330
- options = enter_text_http_options
353
+ # Tries to touch the keyboard delete key, but falls back on typing the
354
+ # backspace character.
355
+ options = enter_text_http_options("\b")
331
356
  parameters = {
332
357
  :gesture => "clear_text"
333
358
  }
@@ -342,7 +367,7 @@ INSTANCE METHODS
342
367
  if !keyboard_visible?
343
368
  raise RuntimeError, "Keyboard must be visible"
344
369
  end
345
- options = enter_text_http_options
370
+ options = enter_text_http_options(string.to_s)
346
371
  parameters = {
347
372
  :gesture => "enter_text",
348
373
  :options => {
@@ -362,7 +387,7 @@ INSTANCE METHODS
362
387
  # 1. Removes duplicate check.
363
388
  # 2. It turns out DeviceAgent query can be very slow.
364
389
  def enter_text_without_keyboard_check(string)
365
- options = enter_text_http_options
390
+ options = enter_text_http_options(string.to_s)
366
391
  parameters = {
367
392
  :gesture => "enter_text",
368
393
  :options => {
@@ -752,6 +777,20 @@ Timed out after #{timeout} seconds waiting for the keyboard to appear.
752
777
  end
753
778
  end
754
779
 
780
+ # @!visibility private
781
+ def wait_for_no_keyboard(timeout=WAIT_DEFAULTS[:timeout])
782
+ options = WAIT_DEFAULTS.dup
783
+ options[:timeout] = timeout
784
+ message = %Q[
785
+
786
+ Timed out after #{timeout} seconds waiting for the keyboard to disappear.
787
+
788
+ ]
789
+ wait_for(message, options) do
790
+ !keyboard_visible?
791
+ end
792
+ end
793
+
755
794
  # @!visibility private
756
795
  def wait_for_alert(timeout=WAIT_DEFAULTS[:timeout])
757
796
  options = WAIT_DEFAULTS.dup
@@ -784,23 +823,52 @@ Timed out after #{timeout} seconds waiting for an alert to disappear.
784
823
  # @!visibility private
785
824
  def wait_for_text_in_view(text, uiquery, options={})
786
825
  merged_options = WAIT_DEFAULTS.merge(options)
787
- result = wait_for_view(uiquery, merged_options)
788
-
789
- # This is not quite right. It is possible to get a false positive.
790
- # If result does not have "value" or "label" and the text is nil
791
- candidates = [result["value"],
792
- result["label"]]
793
- match = candidates.any? do |elm|
794
- elm == text
795
- end
796
- if !match
797
- fail(%Q[
798
826
 
799
- Expected to find '#{text}' as a 'value' or 'label' in
827
+ begin
828
+ wait_for("TMP", merged_options) do
829
+ view = query(uiquery).first
830
+
831
+ if view
832
+ # Guard against this edge case:
833
+ #
834
+ # Text is "" and value or label keys do not exist in view which
835
+ # implies that value or label was the empty string (see the
836
+ # DeviceAgent JSONUtils and Facebook macros).
837
+ if text == "" || text == nil
838
+ view["value"] == nil && view["label"] == nil
839
+ else
840
+ [view["value"], view["label"]].any? { |elm| elm == text }
841
+ end
842
+ else
843
+ false
844
+ end
845
+ end
846
+ rescue merged_options[:exception_class] => e
847
+ view = query(uiquery)
848
+ if !view
849
+ message = %Q[
850
+ Timed out wait after #{merged_options[:timeout]} seconds waiting for a view to match:
800
851
 
801
- #{JSON.pretty_generate(result)}
852
+ #{uiquery}
802
853
 
803
- ])
854
+ ]
855
+ else
856
+ message = %Q[
857
+ Timed out after #{merged_options[:timeout]} seconds waiting for a view matching:
858
+
859
+ '#{uiquery}'
860
+
861
+ to have 'value' or 'label' matching text:
862
+
863
+ '#{text}'
864
+
865
+ Found:
866
+
867
+ #{JSON.pretty_generate(view)}
868
+
869
+ ]
870
+ end
871
+ fail(merged_options[:exception_class], message)
804
872
  end
805
873
  end
806
874
 
@@ -1069,8 +1137,11 @@ PRIVATE
1069
1137
  # @!visibility private
1070
1138
  #
1071
1139
  # A patch while we are trying to figure out what is wrong with text entry.
1072
- def enter_text_http_options
1073
- timeout = DEFAULTS[:http_timeout] * 6
1140
+ def enter_text_http_options(string)
1141
+ characters = string.length + 1
1142
+ characters_per_second = DEFAULTS[:characters_per_second]
1143
+ to_type_timeout = [characters/characters_per_second, 2.0].max
1144
+ timeout = (DEFAULTS[:http_timeout] * 3) + to_type_timeout
1074
1145
  {
1075
1146
  :timeout => timeout,
1076
1147
  :interval => 0.1,
@@ -1178,21 +1249,26 @@ PRIVATE
1178
1249
  hash
1179
1250
  end
1180
1251
 
1181
- # TODO Might not be necessary - this is an edge case and it is likely
1182
- # that iOSDeviceManager will be able to handle this for us.
1252
+ # @!visibility private
1183
1253
  def cbx_runner_stale?
1184
- false
1185
- # The RunLoop::Version class needs to be updated to handle timestamps.
1186
- #
1187
- # if cbx_launcher.name == :xcodebuild
1188
- # return false
1189
- # end
1254
+ return false if RunLoop::Environment.xtc?
1255
+ return false if cbx_launcher.name == :xcodebuild
1256
+ return false if !running?
1257
+
1258
+ version_info = server_version
1259
+ running_version_timestamp = version_info[:bundle_version].to_i
1260
+
1261
+ app = RunLoop::App.new(cbx_launcher.runner.runner)
1262
+ plist_buddy = RunLoop::PlistBuddy.new
1263
+ version_timestamp = plist_buddy.plist_read("CFBundleVersion", app.info_plist_path).to_i
1190
1264
 
1191
- # version_info = server_version
1192
- # running_bundle_version = RunLoop::Version.new(version_info[:bundle_version])
1193
- # bundle_version = RunLoop::App.new(cbx_launcher.runner.runner).bundle_version
1194
- #
1195
- # running_bundle_version < bundle_version
1265
+ if running_version_timestamp == version_timestamp
1266
+ RunLoop.log_debug("The running DeviceAgent version is the same as the version on disk")
1267
+ false
1268
+ else
1269
+ RunLoop.log_debug("The running DeviceAgent version is not the same as the version on disk")
1270
+ true
1271
+ end
1196
1272
  end
1197
1273
 
1198
1274
  # @!visibility private
@@ -1289,7 +1365,7 @@ Please install it.
1289
1365
  client = http_client(http_options)
1290
1366
  request = request("session",
1291
1367
  {
1292
- :bundleID => bundle_id,
1368
+ :bundle_id => bundle_id,
1293
1369
  :launchArgs => aut_args,
1294
1370
  :environment => aut_env
1295
1371
  })