run_loop 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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