run_loop 2.2.4 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  })