run_loop 2.1.1 → 2.1.2

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: 39af0451c6719a47b9adafba2e10d2421633d379
4
- data.tar.gz: 98a21653ec46a7c7d04732ea28a81f30b574a413
3
+ metadata.gz: 153c09febc934625b8890c29fbead9efd47ccec9
4
+ data.tar.gz: def40d52e20c4e6eff000a13b14ebedaf989b11c
5
5
  SHA512:
6
- metadata.gz: ac0327a5a25b1c76095520787c28aa4550474fbebcbf3d213737fd46fc89117dad93e7de1eb491af717627c56bdb932b44a771aed45ec8008d48d1d4d5df2955
7
- data.tar.gz: 04541d16efd91c9f1889869db1e8fbac9364ce72371efb5cd6f1c66e13038a4410b8614ad1a7648da91dc5c56e67f9f2ad47080ed4be04d7fa37f619b7338a28
6
+ metadata.gz: e8e4c05e8d8ffc9287c9f8b9b8c7db070eab11580d6219156f6c25d59938448ef6a617136a54184b6459f7c66f3741352f6234c5d27cdafb3fe4300e2f8403a6
7
+ data.tar.gz: f2896bcd29d50e34bdc418bd3f255bddb8e0adb172263e3a5a8859662ec4aa7437485754a9e35def02b682e069dd768acb97ed5074859ebf82083c3d3d772694
@@ -1,5 +1,7 @@
1
1
  require 'run_loop/regex'
2
2
  require 'run_loop/directory'
3
+ require "run_loop/encoding"
4
+ require "run_loop/shell"
3
5
  require 'run_loop/environment'
4
6
  require 'run_loop/logging'
5
7
  require 'run_loop/dot_dir'
@@ -66,10 +68,25 @@ module RunLoop
66
68
 
67
69
  def self.run(options={})
68
70
 
71
+ cloned_options = options.clone
72
+
73
+ # We want to use the _exact_ objects that were passed.
74
+ if options[:xcode]
75
+ cloned_options[:xcode] = options[:xcode]
76
+ end
77
+
78
+ if options[:simctl]
79
+ cloned_options[:simctl] = options[:simctl]
80
+ end
81
+
82
+ # Soon to be unsupported.
83
+ if options[:sim_control]
84
+ cloned_options[:sim_control] = options[:sim_control]
85
+ end
86
+
69
87
  if options[:xcuitest]
70
- RunLoop::XCUITest.run(options)
88
+ RunLoop::XCUITest.run(cloned_options)
71
89
  else
72
-
73
90
  if RunLoop::Instruments.new.instruments_app_running?
74
91
  raise %q(The Instruments.app is open.
75
92
 
@@ -79,58 +96,6 @@ control of your application.
79
96
  Please quit the Instruments.app and try again.)
80
97
 
81
98
  end
82
-
83
- uia_strategy = options[:uia_strategy]
84
- if options[:script]
85
- script = validate_script(options[:script])
86
- else
87
- if uia_strategy
88
- script = default_script_for_uia_strategy(uia_strategy)
89
- else
90
- if options[:calabash_lite]
91
- uia_strategy = :host
92
- script = Core.script_for_key(:run_loop_host)
93
- else
94
- uia_strategy = :preferences
95
- script = default_script_for_uia_strategy(uia_strategy)
96
- end
97
- end
98
- end
99
- # At this point, 'script' has been chosen, but uia_strategy might not
100
- unless uia_strategy
101
- desired_script = options[:script]
102
- if desired_script.is_a?(String) #custom path to script
103
- uia_strategy = :host
104
- elsif desired_script == :run_loop_host
105
- uia_strategy = :host
106
- elsif desired_script == :run_loop_fast_uia
107
- uia_strategy = :preferences
108
- elsif desired_script == :run_loop_shared_element
109
- uia_strategy = :shared_element
110
- else
111
- raise "Inconsistent state: desired script #{desired_script} has not uia_strategy"
112
- end
113
- end
114
-
115
- # At this point script and uia_strategy selected
116
- cloned_options = options.clone
117
- cloned_options[:script] = script
118
- cloned_options[:uia_strategy] = uia_strategy
119
-
120
- # Xcode and SimControl will not be properly cloned and we don't want
121
- # them to be; we want to use the exact objects that were passed.
122
- if options[:xcode]
123
- cloned_options[:xcode] = options[:xcode]
124
- end
125
-
126
- if options[:sim_control]
127
- cloned_options[:sim_control] = options[:sim_control]
128
- end
129
-
130
- if options[:simctl]
131
- cloned_options[:simctl] = options[:simctl]
132
- end
133
-
134
99
  Core.run_with_options(cloned_options)
135
100
  end
136
101
  end
@@ -212,7 +177,11 @@ Please quit the Instruments.app and try again.)
212
177
  FileUtils.cp(pngs, dest) if pngs and pngs.length > 0
213
178
  end
214
179
 
180
+ # @!visibility private
181
+ #
182
+ # @deprecated since 2.1.2
215
183
  def self.default_script_for_uia_strategy(uia_strategy)
184
+ self.deprecated("2.1.2", "Replaced by methods in RunLoop::Core")
216
185
  case uia_strategy
217
186
  when :preferences
218
187
  Core.script_for_key(:run_loop_fast_uia)
@@ -225,7 +194,11 @@ Please quit the Instruments.app and try again.)
225
194
  end
226
195
  end
227
196
 
197
+ # @!visibility private
198
+ #
199
+ # @deprecated since 2.1.2
228
200
  def self.validate_script(script)
201
+ self.deprecated("2.1.2", "Replaced by methods in RunLoop::Core")
229
202
  if script.is_a?(String)
230
203
  unless File.exist?(script)
231
204
  raise "Unable to find file: #{script}"
@@ -27,10 +27,6 @@ module RunLoop
27
27
  READ_SCRIPT_PATH = File.join(SCRIPTS_PATH, 'read-cmd.sh')
28
28
  TIMEOUT_SCRIPT_PATH = File.join(SCRIPTS_PATH, 'timeout3')
29
29
 
30
- def self.scripts_path
31
- SCRIPTS_PATH
32
- end
33
-
34
30
  def self.log_run_loop_options(options, xcode)
35
31
  return unless RunLoop::Environment.debug?
36
32
  # Ignore :sim_control b/c it is a ruby object; printing is not useful.
@@ -67,10 +63,21 @@ module RunLoop
67
63
  xcode = options[:xcode] || RunLoop::Xcode.new
68
64
  instruments = options[:instruments] || RunLoop::Instruments.new
69
65
 
70
- # Find the Device under test, the App under test, UIA strategy, and reset options
66
+ # Device under test: DUT
71
67
  device = RunLoop::Device.detect_device(options, xcode, simctl, instruments)
68
+
69
+ # App under test: AUT
72
70
  app_details = RunLoop::DetectAUT.detect_app_under_test(options)
73
- uia_strategy = self.detect_uia_strategy(options, device, xcode)
71
+
72
+ # Find the script to pass to instruments and the strategy to communicate
73
+ # with UIAutomation.
74
+ script_n_strategy = self.detect_instruments_script_and_strategy(options,
75
+ device,
76
+ xcode)
77
+ instruments_script = script_n_strategy[:script]
78
+ uia_strategy = script_n_strategy[:strategy]
79
+
80
+ # The app life cycle reset options.
74
81
  reset_options = self.detect_reset_options(options)
75
82
 
76
83
  instruments.kill_instruments(xcode)
@@ -82,14 +89,14 @@ module RunLoop
82
89
  FileUtils.mkdir_p(results_dir_trace)
83
90
 
84
91
  dependencies = options[:dependencies] || []
85
- dependencies << File.join(scripts_path, 'calabash_script_uia.js')
92
+ dependencies << File.join(SCRIPTS_PATH, 'calabash_script_uia.js')
86
93
  dependencies.each do |dep|
87
94
  FileUtils.cp(dep, results_dir)
88
95
  end
89
96
 
90
97
  script = File.join(results_dir, '_run_loop.js')
91
98
 
92
- javascript = UIAScriptTemplate.new(SCRIPTS_PATH, options[:script]).result
99
+ javascript = UIAScriptTemplate.new(SCRIPTS_PATH, instruments_script).result
93
100
  UIAScriptTemplate.sub_path_var!(javascript, results_dir)
94
101
  UIAScriptTemplate.sub_read_script_path_var!(javascript, READ_SCRIPT_PATH)
95
102
  UIAScriptTemplate.sub_timeout_script_path_var!(javascript, TIMEOUT_SCRIPT_PATH)
@@ -137,7 +144,8 @@ module RunLoop
137
144
  :results_dir => results_dir,
138
145
  :script => script,
139
146
  :log_file => log_file,
140
- :args => args
147
+ :args => args,
148
+ :uia_strategy => uia_strategy
141
149
  }
142
150
  merged_options = options.merge(discovered_options)
143
151
 
@@ -314,8 +322,7 @@ Logfile: #{log_file}
314
322
  begin
315
323
  FileUtils.rm_f(repl_path)
316
324
  return repl_path if system(%Q[mkfifo "#{repl_path}"])
317
- rescue Errno::EINTR => e
318
- #retry
325
+ rescue Errno::EINTR => _
319
326
  sleep(0.1)
320
327
  end
321
328
  end
@@ -516,7 +523,7 @@ Logfile: #{log_file}
516
523
  def self.detect_connected_device
517
524
  begin
518
525
  Timeout::timeout(1, RunLoop::TimeoutError) do
519
- return `#{File.join(scripts_path, 'udidetect')}`.chomp
526
+ return `#{File.join(SCRIPTS_PATH, 'udidetect')}`.chomp
520
527
  end
521
528
  rescue RunLoop::TimeoutError => _
522
529
  `killall udidetect &> /dev/null`
@@ -670,6 +677,45 @@ Logfile: #{log_file}
670
677
  strategy
671
678
  end
672
679
 
680
+ # @!visibility private
681
+ #
682
+ # There is an unnatural relationship between the :script and the
683
+ # :uia_strategy keys.
684
+ #
685
+ # @param [Hash] options The launch options passed to .run_with_options
686
+ # @param [RunLoop::Device] device The device under test.
687
+ # @param [RunLoop::Xcode] xcode The active Xcode.
688
+ #
689
+ # @return [Hash] with two keys: :script and :uia_strategy
690
+ def self.detect_instruments_script_and_strategy(options, device, xcode)
691
+ strategy = options[:uia_strategy]
692
+ script = options[:script]
693
+
694
+ if script
695
+ script = self.expect_instruments_script(script)
696
+ if !strategy
697
+ strategy = :host
698
+ end
699
+ else
700
+ if strategy
701
+ script = self.instruments_script_for_uia_strategy(strategy)
702
+ else
703
+ if options[:calabash_lite]
704
+ strategy = :host
705
+ script = self.instruments_script_for_uia_strategy(strategy)
706
+ else
707
+ strategy = self.detect_uia_strategy(options, device, xcode)
708
+ script = self.instruments_script_for_uia_strategy(strategy)
709
+ end
710
+ end
711
+ end
712
+
713
+ {
714
+ :script => script,
715
+ :strategy => strategy
716
+ }
717
+ end
718
+
673
719
  # @!visibility private
674
720
  #
675
721
  # UIAutomation buffers log output in some very strange ways. RunLoop
@@ -760,5 +806,48 @@ Logfile: #{log_file}
760
806
 
761
807
  RunLoop.log_debug("Simulator instruction set '#{device.instruction_set}' is compatible with '#{lipo.info}'")
762
808
  end
809
+
810
+ # @!visibility private
811
+ def self.expect_instruments_script(script)
812
+ if script.is_a?(String)
813
+ unless File.exist?(script)
814
+ raise %Q[Expected instruments JavaScript file at path:
815
+
816
+ #{script}
817
+
818
+ Check the :script key in your launch options.]
819
+ end
820
+ script
821
+ elsif script.is_a?(Symbol)
822
+ path = self.script_for_key(script)
823
+ if !path
824
+ raise %Q[Expected :#{script} to be one of:
825
+
826
+ #{Core::SCRIPTS.keys.map { |key| ":#{key}" }.join("\n")}
827
+
828
+ Check the :script key in your launch options.]
829
+ end
830
+ path
831
+ else
832
+ raise %Q[Expected '#{script}' to be a Symbol or a String.
833
+
834
+ Check the :script key in your launch options.]
835
+ end
836
+ end
837
+
838
+ # @!visibility private
839
+ def self.instruments_script_for_uia_strategy(uia_strategy)
840
+ case uia_strategy
841
+ when :preferences
842
+ self.script_for_key(:run_loop_fast_uia)
843
+ when :host
844
+ self.script_for_key(:run_loop_host)
845
+ when :shared_element
846
+ self.script_for_key(:run_loop_shared_element)
847
+ else
848
+ self.script_for_key(:run_loop_basic)
849
+ end
850
+ end
763
851
  end
764
852
  end
853
+
@@ -847,7 +847,7 @@ Command had no output
847
847
  return true
848
848
  end
849
849
 
850
- RunLoop.log_debug("The app you are are testing is not the same as the app that is installed.")
850
+ RunLoop.log_debug("The app you are testing is not the same as the app that is installed.")
851
851
  RunLoop.log_debug(" Installed app SHA: #{installed_sha}")
852
852
  RunLoop.log_debug(" App to launch SHA: #{app_sha}")
853
853
  RunLoop.log_debug("Will install #{app}")
@@ -42,7 +42,9 @@ module RunLoop
42
42
  def ignore_xcodeproj?(path)
43
43
  path[/CordovaLib/, 0] ||
44
44
  path[/Pods/, 0] ||
45
- path[/Carthage/, 0]
45
+ path[/Carthage/, 0] ||
46
+ path[/Airship(Kit|Lib)/, 0] ||
47
+ path[/google-plus-ios-sdk/, 0]
46
48
  end
47
49
 
48
50
  # @!visibility private
@@ -1,6 +1,7 @@
1
1
  module RunLoop
2
2
  class Device
3
3
 
4
+ require 'securerandom'
4
5
  include RunLoop::Regex
5
6
 
6
7
  # Starting in Xcode 7, iOS 9 simulators have a new "booting" state.
@@ -133,7 +134,7 @@ removed (1.5.0). It has been replaced by an options hash with two keys:
133
134
  #
134
135
  # @param [Hash] options The launch options passed to RunLoop::Core
135
136
  # @param [RunLoop::Xcode] xcode An Xcode instance
136
- # @param [RunLoop::Simctl] simctl A SimControl instance
137
+ # @param [RunLoop::Simctl] simctl A Simctl instance
137
138
  # @param [RunLoop::Instruments] instruments An Instruments instance
138
139
  #
139
140
  # @raise [ArgumentError] If "device" is detected as the device target and
@@ -318,130 +319,111 @@ version: #{version}
318
319
  end.call
319
320
  end
320
321
 
321
- # @!visibility private
322
- # Is this the first launch of this Simulator?
323
- #
324
- # TODO Needs unit and integration tests.
325
- def simulator_first_launch?
326
- megabytes = simulator_data_dir_size
327
-
328
- if version >= RunLoop::Version.new('9.0')
329
- megabytes < 20
330
- elsif version >= RunLoop::Version.new('8.0')
331
- megabytes < 12
332
- else
333
- megabytes < 8
334
- end
335
- end
336
-
337
- # @!visibility private
338
- # The size of the simulator data/ directory.
339
- #
340
- # TODO needs unit tests.
341
- def simulator_data_dir_size
342
- path = File.join(simulator_root_dir, 'data')
343
- RunLoop::Directory.size(path, :mb)
344
- end
345
-
346
322
  # @!visibility private
347
323
  #
348
324
  # Waits for three conditions:
349
325
  #
350
326
  # 1. The SHA sum of the simulator data/ directory to be stable.
351
- # 2. No more log messages are begin generated
352
- # 3. 1 and 2 must hold for 1 seconds.
327
+ # 2. No more log messages are begin generated.
328
+ # 3. 1 and 2 must hold for 1.5 seconds.
353
329
  #
354
- # When the simulator version is >= iOS 9 _and_ it is the first launch of
355
- # the simulator after a reset or a new simulator install, a fourth condition
356
- # is added:
330
+ # When the simulator version is >= iOS 9, two more conditions are added to
331
+ # get past the iOS 9+ boot screen.
357
332
  #
358
- # 4. The first three conditions must be met a second time.
333
+ # 4. Wait for com.apple.audio.SystemSoundServer-iOS-Simulator process to
334
+ # start.
335
+ # 5. 1 and 2 must hold for 1.5 seconds.
359
336
  #
360
- # and the quiet time is increased to 2.0.
337
+ # When the simulator version is >= iOS 9 and the device is an iPad another
338
+ # condition is added because simctl fails to correctly install applications;
339
+ # the app and data container exists, but Springboard does not detect them.
340
+ #
341
+ # 6. 1 and 2 must hold for 1.5 seconds.
361
342
  def simulator_wait_for_stable_state
362
- require 'securerandom'
363
343
 
364
344
  # How long to wait between stability checks.
345
+ # Shorter than this gives false positives.
365
346
  delay = 0.5
366
347
 
367
- first_launch = false
348
+ # How many times to wait for stable state.
349
+ max_stable_count = 3
368
350
 
369
- # At launch there is a brief moment when the SHA and
370
- # the log file are are stable. Then a bunch of activity
371
- # occurs. This is the quiet time.
351
+ # How long to wait for iOS 9 boot screen.
352
+ boot_screen_wait_options = {
353
+ :max_boot_screen_wait => 10,
354
+ :raise_on_timeout => false
355
+ }
356
+
357
+ # How much additional time to wait for iOS 9+ iPads.
372
358
  #
373
- # Starting in iOS 9, simulators display at _booting_ screen
374
- # at first launch. At first launch, these simulators need
375
- # a much longer quiet time.
376
- if version >= RunLoop::Version.new('9.0')
377
- first_launch = simulator_data_dir_size < 20
378
- quiet_time = 2.0
379
- else
380
- quiet_time = 1.0
359
+ # Installing and launching on iPads is problematic.
360
+ # Sometimes the app is installed, but SpringBoard does
361
+ # not recognize that the app is installed even though
362
+ # simctl says that it is.
363
+ additional_ipad_delay = delay * 2
364
+
365
+ # Adjust for CI environments
366
+ if RunLoop::Environment.ci?
367
+ max_stable_count = 5
368
+ boot_screen_wait_options[:max_boot_screen_wait] = 20
369
+ additional_ipad_delay = delay * 4
381
370
  end
382
371
 
383
- now = Time.now
384
- timeout = SIM_STABLE_STATE_OPTIONS[:timeout]
385
- poll_until = now + timeout
386
- quiet = now + quiet_time
372
+ # iOS 9 simulators have an additional boot screen.
373
+ is_gte_ios9 = version >= RunLoop::Version.new('9.0')
387
374
 
388
- is_stable = false
375
+ # iOS 9 iPad simulators need additional time to stabilize.
376
+ is_ipad = simulator_is_ipad?
389
377
 
390
- path = File.join(simulator_root_dir, 'data')
391
- current_sha = nil
392
- sha_fn = lambda do |data_dir|
393
- begin
394
- # Typically, this returns in < 0.3 seconds.
395
- Timeout.timeout(10, TimeoutError) do
396
- # Errors are ignorable and users are confused by the messages.
397
- options = { :handle_errors_by => :ignoring }
398
- RunLoop::Directory.directory_digest(data_dir, options)
399
- end
400
- rescue => _
401
- SecureRandom.uuid
402
- end
403
- end
378
+ timeout = SIM_STABLE_STATE_OPTIONS[:timeout]
379
+ now = Time.now
380
+ poll_until = now + timeout
404
381
 
405
- RunLoop.log_debug("Waiting for simulator to stabilize with timeout: #{timeout}")
406
- if first_launch
407
- RunLoop.log_debug("Detected the first launch of an iOS >= 9.0 Simulator")
408
- end
382
+ RunLoop.log_debug("Waiting for simulator to stabilize with timeout: #{timeout} seconds")
409
383
 
410
- current_line = nil
384
+ current_dir_sha = simulator_data_directory_sha
385
+ current_log_sha = simulator_log_file_sha
386
+ is_stable = false
387
+ waited_for_boot = false
388
+ waited_for_ipad = false
389
+ stable_count = 0
411
390
 
412
391
  while Time.now < poll_until do
413
- latest_sha = sha_fn.call(path)
414
- latest_line = last_line_from_simulator_log_file
392
+ latest_dir_sha = simulator_data_directory_sha
393
+ latest_log_sha = simulator_log_file_sha
415
394
 
416
- is_stable = current_sha == latest_sha && current_line == latest_line
395
+ is_stable = [current_dir_sha == latest_dir_sha,
396
+ current_log_sha == latest_log_sha].all?
417
397
 
418
398
  if is_stable
419
- if Time.now > quiet
420
- if first_launch
421
- RunLoop.log_debug('First launch detected - allowing additional time to stabilize')
422
- first_launch = false
423
- sleep 1.2
424
- quiet = Time.now + quiet_time
399
+ stable_count = stable_count + 1
400
+ if stable_count == max_stable_count
401
+ if is_gte_ios9 && !waited_for_boot
402
+ process_name = "com.apple.audio.SystemSoundServer-iOS-Simulator"
403
+ RunLoop::ProcessWaiter.new(process_name, boot_screen_wait_options).wait_for_any
404
+ waited_for_boot = true
405
+ stable_count = 0
406
+ elsif is_gte_ios9 && is_ipad && !waited_for_ipad
407
+ RunLoop.log_debug("Waiting additional time for iOS 9 iPad to stabilize")
408
+ sleep(additional_ipad_delay)
409
+ waited_for_ipad = true
410
+ stable_count = 0
425
411
  else
426
412
  break
427
413
  end
428
- else
429
- quiet = Time.now + quiet_time
430
414
  end
431
415
  end
432
416
 
433
- current_sha = latest_sha
434
- current_line = latest_line
435
- sleep delay
417
+ current_dir_sha = latest_dir_sha
418
+ current_log_sha = latest_log_sha
419
+ sleep(delay)
436
420
  end
437
421
 
438
422
  if is_stable
439
423
  elapsed = Time.now - now
440
- stabilized = elapsed - quiet_time
441
- RunLoop.log_debug("Simulator stable after #{stabilized} seconds")
442
424
  RunLoop.log_debug("Waited a total of #{elapsed} seconds for simulator to stabilize")
443
425
  else
444
- RunLoop.log_debug("Timed out: simulator not stable after #{timeout} seconds")
426
+ RunLoop.log_debug("Timed out after #{timeout} seconds waiting for simulator to stabilize")
445
427
  end
446
428
  end
447
429
 
@@ -530,33 +512,6 @@ version: #{version}
530
512
 
531
513
  private
532
514
 
533
- # @!visibility private
534
- # TODO write a unit test.
535
- def last_line_from_simulator_log_file
536
- file = simulator_log_file_path
537
-
538
- return nil if !File.exist?(file)
539
-
540
- debug = RunLoop::Environment.debug?
541
-
542
- begin
543
- io = File.open(file, 'r')
544
- io.seek(-100, IO::SEEK_END)
545
-
546
- line = io.readline
547
- rescue StandardError => e
548
- RunLoop.log_error("Caught #{e} while reading simulator log file") if debug
549
- ensure
550
- io.close if io && !io.closed?
551
- end
552
-
553
- if line
554
- line.chomp
555
- else
556
- line
557
- end
558
- end
559
-
560
515
  # @!visibility private
561
516
  def xcrun
562
517
  RunLoop::Xcrun.new
@@ -650,6 +605,50 @@ version: #{version}
650
605
  udid
651
606
  end
652
607
 
608
+ # @!visibility private
609
+ def simulator_data_directory_sha
610
+ path = File.join(simulator_root_dir, 'data')
611
+ begin
612
+ # Typically, this returns in < 0.3 seconds.
613
+ Timeout.timeout(10, TimeoutError) do
614
+ # Errors are ignorable and users are confused by the messages.
615
+ options = { :handle_errors_by => :ignoring }
616
+ RunLoop::Directory.directory_digest(path, options)
617
+ end
618
+ rescue => _
619
+ SecureRandom.uuid
620
+ end
621
+ end
622
+
623
+ # @!visibility private
624
+ def simulator_log_file_sha
625
+ file = simulator_log_file_path
626
+
627
+ return nil if !File.exist?(file)
628
+
629
+ sha = OpenSSL::Digest::SHA256.new
630
+
631
+ begin
632
+ sha << File.read(file)
633
+ rescue => _
634
+ sha = SecureRandom.uuid
635
+ end
636
+
637
+ sha
638
+ end
639
+
640
+ # @!visibility private
641
+ # Value of <UDID>/.device.plist 'deviceType' key.
642
+ def simulator_device_type
643
+ plist = File.join(simulator_device_plist)
644
+ pbuddy.plist_read("deviceType", plist)
645
+ end
646
+
647
+ # @!visibility private
648
+ def simulator_is_ipad?
649
+ simulator_device_type[/iPad/, 0]
650
+ end
651
+
653
652
  # @!visibility private
654
653
  def self.ensure_physical_device_connected(identifier, options)
655
654
  if identifier.nil?
@@ -0,0 +1,39 @@
1
+
2
+ module RunLoop
3
+ module Encoding
4
+
5
+ # Raised when a string cannot be coerced to UTF8
6
+ class UTF8Error < RuntimeError; end
7
+
8
+ # @!visibility private
9
+ def ensure_command_output_utf8(string, command)
10
+ return '' if !string
11
+
12
+ utf8 = string.force_encoding("UTF-8").chomp
13
+
14
+ return utf8 if utf8.valid_encoding?
15
+
16
+ encoded = utf8.encode("UTF-8", "UTF-8",
17
+ invalid: :replace,
18
+ undef: :replace,
19
+ replace: "")
20
+
21
+ return encoded if encoded.valid_encoding?
22
+
23
+ raise UTF8Error, %Q{
24
+ Could not force UTF-8 encoding on this string:
25
+
26
+ #{string}
27
+
28
+ which is the output of this command:
29
+
30
+ #{command}
31
+
32
+ Please file an issue with a stacktrace and the text of this error.
33
+
34
+ https://github.com/calabash/run_loop/issues
35
+ }
36
+ end
37
+ end
38
+ end
39
+
@@ -15,7 +15,11 @@ module RunLoop
15
15
 
16
16
  # Returns true if Windows environment
17
17
  def self.windows_env?
18
- RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
18
+ if @@windows_env.nil?
19
+ @@windows_env = Environment.host_os_is_win?
20
+ end
21
+
22
+ @@windows_env
19
23
  end
20
24
 
21
25
  # Returns true if debugging is enabled.
@@ -256,5 +260,26 @@ Check your environment.]
256
260
  path
257
261
  end
258
262
  end
263
+
264
+ private
265
+
266
+ # @visibility private
267
+ WIN_PATTERNS = [
268
+ /bccwin/i,
269
+ /cygwin/i,
270
+ /djgpp/i,
271
+ /mingw/i,
272
+ /mswin/i,
273
+ /wince/i,
274
+ ]
275
+
276
+ # @!visibility private
277
+ @@windows_env = nil
278
+
279
+ # @!visibility private
280
+ def self.host_os_is_win?
281
+ ruby_platform = RbConfig::CONFIG["host_os"]
282
+ !!WIN_PATTERNS.find { |r| ruby_platform =~ r }
283
+ end
259
284
  end
260
285
  end
@@ -0,0 +1,221 @@
1
+ module RunLoop
2
+ # @!visibility private
3
+ module PhysicalDevice
4
+
5
+ # @!visibility private
6
+ class IDeviceInstaller < LifeCycle
7
+
8
+ # Is the tool installed?
9
+ def self.tool_is_installed?
10
+ raise RunLoop::Abstract::AbstractMethodError,
11
+ "Subclass must implement '.tool_is_installed?'"
12
+ end
13
+
14
+ # Path to tool.
15
+ def self.executable_path
16
+ raise RunLoop::Abstract::AbstractMethodError,
17
+ "Subclass must implement '.executable_path'"
18
+ end
19
+
20
+ # Is the app installed?
21
+ #
22
+ # @return [Boolean] true or false
23
+ def app_installed?(bundle_id)
24
+ abstract_method!
25
+ end
26
+
27
+ # Install the app or ipa.
28
+ #
29
+ # If the app is already installed, it will be reinstalled from disk;
30
+ # no version check is performed.
31
+ #
32
+ # App data is never preserved. If you want to preserve the app data,
33
+ # call `ensure_app_installed`.
34
+ #
35
+ # Possible return values:
36
+ #
37
+ # * :reinstalled => app was installed, but app data was not preserved.
38
+ # * :installed => app was not installed.
39
+ #
40
+ # @raise [InstallError] If app was not installed.
41
+ # @return [Symbol] A keyword describing the action that was performed.
42
+ def install_app(app_or_ipa)
43
+ abstract_method!
44
+ end
45
+
46
+ # Uninstall the app with bundle_id.
47
+ #
48
+ # App data is never preserved. If you want to install a new version of
49
+ # an app and preserve app data (upgrade testing), call
50
+ # `ensure_app_installed`.
51
+ #
52
+ # Possible return values:
53
+ #
54
+ # * :nothing => app was not installed
55
+ # * :uninstall => app was uninstalled
56
+ #
57
+ # @raise [UninstallError] If the app cannot be uninstalled, usually
58
+ # because it is a system app.
59
+ # @return [Symbol] A keyword that describes what action was performed.
60
+ def uninstall_app(bundle_id)
61
+ abstract_method!
62
+ end
63
+
64
+ # Ensures the app is installed and ensures that app is not stale by
65
+ # asking if the version of installed app is different than the version
66
+ # of the app or ipa on disk.
67
+ #
68
+ # The concrete implementation needs to check the CFBundleVersion and
69
+ # the CFBundleShortVersionString. If either are different, then the
70
+ # app should be reinstalled.
71
+ #
72
+ # If possible, the app data should be preserved across reinstallation.
73
+ #
74
+ # Possible return values:
75
+ #
76
+ # * :nothing => app was already installed and versions matched.
77
+ # * :upgraded => app was stale; newer version from disk was installed and
78
+ # app data was preserved.
79
+ # * :reinstalled => app was stale; newer version from disk was installed,
80
+ # but app data was not preserved.
81
+ # * :installed => app was not installed.
82
+ #
83
+ # @raise [InstallError] If the app could not be installed.
84
+ # @raise [UninstallError] If the app could not be uninstalled.
85
+ #
86
+ # @return [Symbol] A keyword that describes the action that was taken.
87
+ def ensure_app_installed(app_or_ipa)
88
+ abstract_method!
89
+ end
90
+
91
+ # Is the app on disk the same as the installed app?
92
+ #
93
+ # The concrete implementation needs to check the CFBundleVersion and
94
+ # the CFBundleShortVersionString. If either are different, then this
95
+ # method returns false.
96
+ #
97
+ # @raise [RuntimeError] If app is not already installed.
98
+ def installed_app_same_as?(app_or_ipa)
99
+ abstract_method!
100
+ end
101
+
102
+ # Clear the app sandbox.
103
+ #
104
+ # This method will never uninstall the app. If the concrete
105
+ # implementation cannot reset the app data, this method should raise
106
+ # an exception.
107
+ #
108
+ # Does not clear Keychain. Use the Calabash iOS Keychain API.
109
+ def reset_app_sandbox(bundle_id)
110
+ abstract_method!
111
+ end
112
+
113
+ # Return the architecture of the device.
114
+ def architecture
115
+ abstract_method!
116
+ end
117
+
118
+ # Is the app or ipa compatible with the architecture of the device?
119
+ def app_has_compatible_architecture?(app_or_ipa)
120
+ abstract_method!
121
+ end
122
+
123
+ # Return true if the device is an iPhone.
124
+ def iphone?
125
+ abstract_method!
126
+ end
127
+
128
+ # Return false if the device is an iPad.
129
+ def ipad?
130
+ abstract_method!
131
+ end
132
+
133
+ # Return the model of the device.
134
+ def model
135
+ abstract_method!
136
+ end
137
+
138
+ # Sideload data into the app's sandbox.
139
+ #
140
+ # These directories exist in the application sandbox.
141
+ #
142
+ # * sandbox/Documents
143
+ # * sandbox/Library
144
+ # * sandbox/Preferences
145
+ # * sandbox/tmp
146
+ #
147
+ # The data is a hash of arrays that define source/target file
148
+ # path pairs.
149
+ #
150
+ # {
151
+ # :documents => [
152
+ # {
153
+ # :source => "path/to/file/on/disk",
154
+ # :target => "sub/dir/under/Documents"
155
+ # },
156
+ # {
157
+ # :source => "path/to/other/file",
158
+ # :target => "./"
159
+ # }
160
+ # ],
161
+ #
162
+ # :library => [ < ditto >],
163
+ # :preferences => [ < ditto > ],
164
+ # :tmp => [ < ditto >]
165
+ # }
166
+ #
167
+ # * If a file exists at a target path, it will be replaced.
168
+ # * Subdirectories will be created as necessary.
169
+ # * :source files must exist.
170
+ def sideload(data)
171
+ abstract_method!
172
+ end
173
+
174
+ # Removes a file or directory from the app sandbox.
175
+ #
176
+ # If the path does not exist, no error will be raised.
177
+ #
178
+ # Documents, Library, Preferences, and tmp directories will be
179
+ # deleted, but then recreated. For example:
180
+ #
181
+ # remove_file_from_sandbox("Preferences")
182
+ #
183
+ # The Preferences directory will be deleted and then recreated.
184
+ def remove_from_sandbox(path)
185
+ abstract_method!
186
+ end
187
+
188
+ # @!visibility private
189
+ def expect_app_or_ipa(app_or_ipa)
190
+ if ![is_app?(app_or_ipa), is_ipa?(app_or_ipa)].any?
191
+ if app_or_ipa.nil?
192
+ object = "nil"
193
+ elsif app_or_ipa == ""
194
+ object = "<empty string>"
195
+ else
196
+ object = app_or_ipa
197
+ end
198
+
199
+ raise ArgumentError, %Q[Expected:
200
+
201
+ #{object}
202
+
203
+ to be a RunLoop::App or a RunLoop::Ipa.]
204
+ end
205
+
206
+ true
207
+ end
208
+
209
+ # @!visibility private
210
+ def is_app?(app_or_ipa)
211
+ app_or_ipa.is_a?(RunLoop::App)
212
+ end
213
+
214
+ # @!visibility private
215
+ def is_ipa?(app_or_ipa)
216
+ app_or_ipa.is_a?(RunLoop::Ipa)
217
+ end
218
+ end
219
+ end
220
+ end
221
+
@@ -0,0 +1,103 @@
1
+ module RunLoop
2
+ module Shell
3
+
4
+ require "command_runner"
5
+ require "run_loop/encoding"
6
+ include RunLoop::Encoding
7
+
8
+ # Controls the behavior of Shell#exec.
9
+ #
10
+ # You can override these values if they do not work in your environment.
11
+ #
12
+ # For cucumber users, the best place to override would be in your
13
+ # features/support/env.rb.
14
+ #
15
+ # For example:
16
+ #
17
+ # RunLoop::Shell::DEFAULT_OPTIONS[:timeout] = 60
18
+ DEFAULT_OPTIONS = {
19
+ :timeout => 30,
20
+ :log_cmd => false
21
+ }
22
+
23
+ # Raised when shell command fails.
24
+ class Error < RuntimeError; end
25
+
26
+ # Raised when shell command times out.
27
+ class TimeoutError < RuntimeError; end
28
+
29
+ def exec(args, options={})
30
+
31
+ merged_options = DEFAULT_OPTIONS.merge(options)
32
+
33
+ timeout = merged_options[:timeout]
34
+
35
+ unless args.is_a?(Array)
36
+ raise ArgumentError,
37
+ "Expected args '#{args}' to be an Array, but found '#{args.class}'"
38
+ end
39
+
40
+ args.each do |arg|
41
+ unless arg.is_a?(String)
42
+ raise ArgumentError,
43
+ %Q{Expected arg '#{arg}' to be a String, but found '#{arg.class}'
44
+ IO.popen requires all arguments to be Strings.
45
+ }
46
+ end
47
+ end
48
+
49
+ cmd = "#{args.join(' ')}"
50
+
51
+ # Don't see your log?
52
+ # Commands are only logged when debugging.
53
+ RunLoop.log_unix_cmd(cmd) if merged_options[:log_cmd]
54
+
55
+ hash = {}
56
+
57
+ begin
58
+
59
+ start_time = Time.now
60
+ command_output = CommandRunner.run(args, timeout: timeout)
61
+
62
+ out = ensure_command_output_utf8(command_output[:out], cmd)
63
+ process_status = command_output[:status]
64
+
65
+ hash =
66
+ {
67
+ :out => out,
68
+ :pid => process_status.pid,
69
+ # nil if process was killed before completion
70
+ :exit_status => process_status.exitstatus
71
+ }
72
+
73
+ rescue RunLoop::Encoding::UTF8Error => e
74
+ raise e
75
+ rescue => e
76
+ elapsed = "%0.2f" % (Time.now - start_time)
77
+ raise Error,
78
+ %Q{Encountered an error after #{elapsed} seconds:
79
+
80
+ #{e.message}
81
+
82
+ executing this command:
83
+
84
+ #{cmd}
85
+ }
86
+ end
87
+
88
+ if hash[:exit_status].nil?
89
+ elapsed = "%0.2f" % (Time.now - start_time)
90
+ raise TimeoutError,
91
+ %Q{Timed out after #{elapsed} seconds executing
92
+
93
+ #{cmd}
94
+
95
+ with a timeout of #{timeout}
96
+ }
97
+ end
98
+
99
+ hash
100
+ end
101
+ end
102
+ end
103
+
@@ -1,5 +1,5 @@
1
1
  module RunLoop
2
- VERSION = "2.1.1"
2
+ VERSION = "2.1.2"
3
3
 
4
4
  # A model of a software release version that can be used to compare two versions.
5
5
  #
@@ -1,7 +1,9 @@
1
1
  module RunLoop
2
2
  class Xcrun
3
3
 
4
- require 'command_runner'
4
+ require "command_runner"
5
+ require "run_loop/encoding"
6
+ include RunLoop::Encoding
5
7
 
6
8
  # Controls the behavior of Xcrun#exec.
7
9
  #
@@ -21,9 +23,6 @@ module RunLoop
21
23
  # Raised when Xcrun fails.
22
24
  class Error < RuntimeError; end
23
25
 
24
- # Raised when the output of the command cannot be coerced to UTF8
25
- class UTF8Error < RuntimeError; end
26
-
27
26
  # Raised when Xcrun times out.
28
27
  class TimeoutError < RuntimeError; end
29
28
 
@@ -60,7 +59,7 @@ IO.popen requires all arguments to be Strings.
60
59
  start_time = Time.now
61
60
  command_output = CommandRunner.run(['xcrun'] + args, timeout: timeout)
62
61
 
63
- out = encode_utf8_or_raise(command_output[:out], cmd)
62
+ out = ensure_command_output_utf8(command_output[:out], cmd)
64
63
  process_status = command_output[:status]
65
64
 
66
65
  hash =
@@ -71,7 +70,7 @@ IO.popen requires all arguments to be Strings.
71
70
  :exit_status => process_status.exitstatus
72
71
  }
73
72
 
74
- rescue UTF8Error => e
73
+ rescue RunLoop::Encoding::UTF8Error => e
75
74
  raise e
76
75
  rescue => e
77
76
  elapsed = "%0.2f" % (Time.now - start_time)
@@ -99,35 +98,6 @@ with a timeout of #{timeout}
99
98
 
100
99
  hash
101
100
  end
102
-
103
- private
104
-
105
- # @!visibility private
106
- def encode_utf8_or_raise(string, command)
107
- return '' if !string
108
-
109
- utf8 = string.force_encoding("UTF-8").chomp
110
-
111
- return utf8 if utf8.valid_encoding?
112
-
113
- encoded = utf8.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: '')
114
-
115
- return encoded if encoded.valid_encoding?
116
-
117
- raise UTF8Error, %Q{
118
- Could not force UTF-8 encoding on this string:
119
-
120
- #{string}
121
-
122
- which is the output of this command:
123
-
124
- #{command}
125
-
126
- Please file an issue with a stacktrace and the text of this error.
127
-
128
- https://github.com/calabash/run_loop/issues
129
- }
130
- end
131
101
  end
132
102
  end
133
103
 
@@ -126,7 +126,7 @@ module RunLoop
126
126
  # @!visibility private
127
127
  def query(mark)
128
128
  options = http_options
129
- parameters = { :text => mark }
129
+ parameters = { :id => mark }
130
130
  request = request("query", parameters)
131
131
  client = client(options)
132
132
  response = client.post(request)
@@ -135,10 +135,19 @@ module RunLoop
135
135
 
136
136
  # @!visibility private
137
137
  def tap_mark(mark)
138
+ body = query(mark)
139
+ tap_query_result(body)
140
+ end
141
+
142
+ # @!visibility private
143
+ def tap_coordinate(x, y)
138
144
  options = http_options
139
145
  parameters = {
140
- :gesture => "tap",
141
- :text => mark
146
+ :gesture => "touch",
147
+ :specifiers => {
148
+ :coordinate => {x: x, y: y}
149
+ },
150
+ :options => {}
142
151
  }
143
152
  request = request("gesture", parameters)
144
153
  client(options)
@@ -147,14 +156,31 @@ module RunLoop
147
156
  end
148
157
 
149
158
  # @!visibility private
150
- def tap_coordinate(x, y)
151
- options = http_options
159
+ def rotate_home_button_to(position, sleep_for=1.0)
160
+ orientation = orientation_for_position(position)
161
+ parameters = {
162
+ :orientation => orientation
163
+ }
164
+ request = request("rotate_home_button_to", parameters)
165
+ client(http_options)
166
+ response = client.post(request)
167
+ json = expect_200_response(response)
168
+ sleep(sleep_for)
169
+ json
170
+ end
171
+
172
+ # @!visibility private
173
+ def perform_coordinate_gesture(gesture, x, y, options={})
152
174
  parameters = {
153
- :gesture => "tap_coordinate",
154
- :coordinate => {x: x, y: y}
175
+ :gesture => gesture,
176
+ :specifiers => {
177
+ :coordinate => {x: x, y: y}
178
+ },
179
+ :options => options
155
180
  }
181
+
156
182
  request = request("gesture", parameters)
157
- client(options)
183
+ client(http_options)
158
184
  response = client.post(request)
159
185
  expect_200_response(response)
160
186
  end
@@ -273,7 +299,7 @@ module RunLoop
273
299
  5.times do
274
300
  begin
275
301
  health
276
- sleep(0.2)
302
+ sleep(1.0)
277
303
  rescue => _
278
304
  break
279
305
  end
@@ -299,6 +325,10 @@ module RunLoop
299
325
 
300
326
  # @!visibility private
301
327
  def xcodebuild
328
+ env = {
329
+ "COMMAND_LINE_BUILD" => "1"
330
+ }
331
+
302
332
  args = [
303
333
  "xcrun",
304
334
  "xcodebuild",
@@ -318,10 +348,10 @@ module RunLoop
318
348
  :err => log_file
319
349
  }
320
350
 
321
- command = args.join(" ")
351
+ command = "#{env.map.each { |k, v| "#{k}=#{v}" }.join(" ")} #{args.join(" ")}"
322
352
  RunLoop.log_unix_cmd("#{command} >& #{log_file}")
323
353
 
324
- pid = Process.spawn(*args, options)
354
+ pid = Process.spawn(env, *args, options)
325
355
  Process.detach(pid)
326
356
  pid
327
357
  end
@@ -334,6 +364,9 @@ module RunLoop
334
364
 
335
365
  shutdown
336
366
 
367
+ # Temp measure; we need to manage the xcodebuild pids.
368
+ system("pkill xcodebuild")
369
+
337
370
  if device.simulator?
338
371
  # quits the simulator
339
372
  sim = CoreSimulator.new(device, "")
@@ -361,7 +394,9 @@ module RunLoop
361
394
  RunLoop.log_debug("Launched #{bundle_id} on #{device}")
362
395
  RunLoop.log_debug("#{response.body}")
363
396
  if device.simulator?
364
- device.simulator_wait_for_stable_state
397
+ # It is not clear yet whether we should do this. There is a problem
398
+ # in the simulator_wait_for_stable_state; it waits too long.
399
+ # device.simulator_wait_for_stable_state
365
400
  end
366
401
  expect_200_response(response)
367
402
  rescue => e
@@ -409,6 +444,28 @@ Server replied with:
409
444
 
410
445
  path
411
446
  end
447
+
448
+ # @!visibility private
449
+ def orientation_for_position(position)
450
+ symbol = position.to_sym
451
+
452
+ case symbol
453
+ when :down, :bottom
454
+ return 1
455
+ when :up, :top
456
+ return 2
457
+ when :right
458
+ return 3
459
+ when :left
460
+ return 4
461
+ else
462
+ raise ArgumentError, %Q[
463
+ Could not coerce '#{position}' into a valid orientation.
464
+
465
+ Valid values are: :down, :up, :right, :left, :bottom, :top
466
+ ]
467
+ end
468
+ end
412
469
  end
413
470
  end
414
471
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: run_loop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karl Krukow
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-17 00:00:00.000000000 Z
11
+ date: 2016-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -212,6 +212,20 @@ dependencies:
212
212
  - - "~>"
213
213
  - !ruby/object:Gem::Version
214
214
  version: '2.0'
215
+ - !ruby/object:Gem::Dependency
216
+ name: listen
217
+ requirement: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - '='
220
+ - !ruby/object:Gem::Version
221
+ version: 3.0.6
222
+ type: :development
223
+ prerelease: false
224
+ version_requirements: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - '='
227
+ - !ruby/object:Gem::Version
228
+ version: 3.0.6
215
229
  - !ruby/object:Gem::Dependency
216
230
  name: growl
217
231
  requirement: !ruby/object:Gem::Requirement
@@ -304,6 +318,7 @@ files:
304
318
  - lib/run_loop/directory.rb
305
319
  - lib/run_loop/dot_dir.rb
306
320
  - lib/run_loop/dylib_injector.rb
321
+ - lib/run_loop/encoding.rb
307
322
  - lib/run_loop/environment.rb
308
323
  - lib/run_loop/fifo.rb
309
324
  - lib/run_loop/host_cache.rb
@@ -321,10 +336,12 @@ files:
321
336
  - lib/run_loop/logging.rb
322
337
  - lib/run_loop/otool.rb
323
338
  - lib/run_loop/patches/awesome_print.rb
339
+ - lib/run_loop/physical_device/ideviceinstaller.rb
324
340
  - lib/run_loop/plist_buddy.rb
325
341
  - lib/run_loop/process_terminator.rb
326
342
  - lib/run_loop/process_waiter.rb
327
343
  - lib/run_loop/regex.rb
344
+ - lib/run_loop/shell.rb
328
345
  - lib/run_loop/sim_control.rb
329
346
  - lib/run_loop/simctl.rb
330
347
  - lib/run_loop/strings.rb
@@ -367,7 +384,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
367
384
  version: '0'
368
385
  requirements: []
369
386
  rubyforge_project:
370
- rubygems_version: 2.5.2
387
+ rubygems_version: 2.5.1
371
388
  signing_key:
372
389
  specification_version: 4
373
390
  summary: The bridge between Calabash iOS and Xcode command-line tools like instruments