run_loop 1.5.6 → 2.0.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.
@@ -3,6 +3,33 @@ module RunLoop
3
3
 
4
4
  include RunLoop::Regex
5
5
 
6
+ # Starting in Xcode 7, iOS 9 simulators have a new "booting" state.
7
+ #
8
+ # The simulator must completely boot before run-loop tries to do things
9
+ # like installing an app or clearing an app sandbox. Run-loop tries to
10
+ # wait for a the simulator stabilize by watching the checksum of the
11
+ # simulator directory and the simulator log.
12
+ #
13
+ # On resource constrained devices or CI systems, the default settings may
14
+ # not work.
15
+ #
16
+ # You can override these values if they do not work in your environment.
17
+ #
18
+ # For cucumber users, the best place to override would be in your
19
+ # features/support/env.rb.
20
+ #
21
+ # For example:
22
+ #
23
+ # RunLoop::Device::SIM_STABLE_STATE_OPTIONS[:timeout] = 60
24
+ SIM_STABLE_STATE_OPTIONS = {
25
+ # The maximum amount of time to wait for the simulator
26
+ # to stabilize. No errors are raised if this timeout is
27
+ # exceeded - if the default 30 seconds has passed, the
28
+ # simulator is probably stable enough for subsequent
29
+ # operations.
30
+ :timeout => RunLoop::Environment.ci? ? 120 : 30
31
+ }
32
+
6
33
  attr_reader :name
7
34
  attr_reader :version
8
35
  attr_reader :udid
@@ -117,22 +144,12 @@ Please update your sources.))
117
144
  # Returns and instruments-ready device identifier that is a suitable value
118
145
  # for DEVICE_TARGET environment variable.
119
146
  #
120
- # @note As of 1.5.0, the XCTools optional argument has become a non-optional
121
- # Xcode argument.
122
- #
123
- # @param [RunLoop::Xcode, RunLoop::XCTools] xcode The version of the active
147
+ # @param [RunLoop::Xcode] xcode The version of the active
124
148
  # Xcode.
125
149
  # @return [String] An instruments-ready device identifier.
126
150
  # @raise [RuntimeError] If trying to obtain a instruments-ready identifier
127
151
  # for a simulator when Xcode < 6.
128
152
  def instruments_identifier(xcode=SIM_CONTROL.xcode)
129
- if xcode.is_a?(RunLoop::XCTools)
130
- RunLoop.deprecated('1.5.0',
131
- %q(
132
- RunLoop::XCTools has been replaced with a non-optional RunLoop::Xcode argument.
133
- Please update your sources to pass an instance of RunLoop::Xcode))
134
- end
135
-
136
153
  if physical_device?
137
154
  udid
138
155
  else
@@ -292,19 +309,27 @@ Please update your sources to pass an instance of RunLoop::Xcode))
292
309
  def simulator_wait_for_stable_state
293
310
  require 'securerandom'
294
311
 
312
+ # How long to wait between stability checks.
295
313
  delay = 0.5
296
314
 
297
315
  first_launch = false
298
316
 
317
+ # At launch there is a brief moment when the SHA and
318
+ # the log file are are stable. Then a bunch of activity
319
+ # occurs. This is the quiet time.
320
+ #
321
+ # Starting in iOS 9, simulators display at _booting_ screen
322
+ # at first launch. At first launch, these simulators need
323
+ # a much longer quiet time.
299
324
  if version >= RunLoop::Version.new('9.0')
300
325
  first_launch = simulator_data_dir_size < 20
301
- quiet_time = 2
326
+ quiet_time = 2.0
302
327
  else
303
- quiet_time = 1
328
+ quiet_time = 1.0
304
329
  end
305
330
 
306
331
  now = Time.now
307
- timeout = 30
332
+ timeout = SIM_STABLE_STATE_OPTIONS[:timeout]
308
333
  poll_until = now + timeout
309
334
  quiet = now + quiet_time
310
335
 
@@ -315,14 +340,21 @@ Please update your sources to pass an instance of RunLoop::Xcode))
315
340
  sha_fn = lambda do |data_dir|
316
341
  begin
317
342
  # Typically, this returns in < 0.3 seconds.
318
- Timeout.timeout(2, TimeoutError) do
319
- RunLoop::Directory.directory_digest(data_dir)
343
+ Timeout.timeout(10, TimeoutError) do
344
+ # Errors are ignorable and users are confused by the messages.
345
+ options = { :handle_errors_by => :ignoring }
346
+ RunLoop::Directory.directory_digest(data_dir, options)
320
347
  end
321
348
  rescue => _
322
349
  SecureRandom.uuid
323
350
  end
324
351
  end
325
352
 
353
+ RunLoop.log_debug("Waiting for simulator to stabilize with timeout: #{timeout}")
354
+ if first_launch
355
+ RunLoop.log_debug("Detected the first launch of an iOS >= 9.0 Simulator")
356
+ end
357
+
326
358
  current_line = nil
327
359
 
328
360
  while Time.now < poll_until do
@@ -22,8 +22,27 @@ module RunLoop
22
22
  # Computes the digest of directory.
23
23
  #
24
24
  # @param path A path to a directory.
25
+ # @param options Control the behavior of the method.
26
+ # @option options :handle_errors_by (:raising) Controls what to do when
27
+ # File.read causes an error. The default behavior is to raise. Other
28
+ # options are: :logging and :ignoring. Logging will only happen if
29
+ # running in debug mode.
30
+ #
25
31
  # @raise ArgumentError When `path` is not a directory or path does not exist.
26
- def self.directory_digest(path)
32
+ # @raise ArgumentError When options[:handle_errors_by] has n unsupported value.
33
+ def self.directory_digest(path, options={})
34
+ default_options = {
35
+ :handle_errors_by => :raising
36
+ }
37
+
38
+ merged_options = default_options.merge(options)
39
+ handle_errors_by = merged_options[:handle_errors_by]
40
+ unless [:raising, :logging, :ignoring].include?(handle_errors_by)
41
+ raise ArgumentError,
42
+ %Q{Expected :handle_errors_by to be :raising, :logging, or :ignoring;
43
+ found '#{handle_errors_by}'
44
+ }
45
+ end
27
46
 
28
47
  unless File.exist?(path)
29
48
  raise ArgumentError, "Expected '#{path}' to exist"
@@ -47,21 +66,26 @@ module RunLoop
47
66
  begin
48
67
  sha << File.read(file)
49
68
  rescue => e
50
- if debug
51
- RunLoop.log_warn(%Q{
52
- RunLoop::Directory.directory_digest raised an error:
53
-
54
- #{e}
55
-
56
- while trying to find the SHA of this file:
57
-
58
- #{file}
59
-
60
- Please report this here:
61
-
62
- https://github.com/calabash/run_loop/issues
63
-
64
- })
69
+ case handle_errors_by
70
+ when :logging
71
+ message =
72
+ %Q{RunLoop::Directory.directory_digest raised an error:
73
+
74
+ #{e}
75
+
76
+ while trying to find the SHA of this file:
77
+
78
+ #{file}
79
+
80
+ This is not a fatal error; it can be ignored.
81
+ }
82
+ RunLoop.log_debug(message)
83
+ when :raising
84
+ raise e.class, e.message
85
+ when :ignoring
86
+ # nop
87
+ else
88
+ # nop
65
89
  end
66
90
  end
67
91
  end
@@ -7,6 +7,16 @@ module RunLoop
7
7
  # Injects dylibs into running executables using lldb.
8
8
  class DylibInjector
9
9
 
10
+ # Options for controlling how often to retry dylib injection.
11
+ #
12
+ # Try 3 times for 10 seconds each try with a sleep of 2 seconds
13
+ # between tries.
14
+ RETRY_OPTIONS = {
15
+ :tries => 3,
16
+ :interval => 2,
17
+ :timeout => 10
18
+ }
19
+
10
20
  # @!attribute [r] process_name
11
21
  # The name of the process to inject the dylib into. This should be obtained
12
22
  # by inspecting the Info.plist in the app bundle.
@@ -18,6 +28,9 @@ module RunLoop
18
28
  # @return [String] The dylib_path
19
29
  attr_reader :dylib_path
20
30
 
31
+ # @!visibility private
32
+ attr_reader :xcrun
33
+
21
34
  # Create a new dylib injector.
22
35
  # @param [String] process_name The name of the process to inject the dylib
23
36
  # into. This should be obtained by inspecting the Info.plist in the app
@@ -25,92 +38,99 @@ module RunLoop
25
38
  # @param [String] dylib_path The path the dylib to inject.
26
39
  def initialize(process_name, dylib_path)
27
40
  @process_name = process_name
28
- @dylib_path = dylib_path
41
+ @dylib_path = Shellwords.shellescape(dylib_path)
42
+ end
43
+
44
+ def xcrun
45
+ @xcrun ||= RunLoop::Xcrun.new
29
46
  end
30
47
 
31
48
  # Injects a dylib into a a currently running process.
32
- def inject_dylib
33
- debug_logging = RunLoop::Environment.debug?
34
- puts "Starting lldb." if debug_logging
35
-
36
- stderr_output = nil
37
- lldb_status = nil
38
- lldb_start_time = Time.now
39
- Open3.popen3('sh') do |stdin, stdout, stderr, process_status|
40
- stdin.puts 'xcrun lldb --no-lldbinit<<EOF'
41
- stdin.puts "process attach -n '#{@process_name}'"
42
- stdin.puts "expr (void*)dlopen(\"#{@dylib_path}\", 0x2)"
43
- stdin.puts 'detach'
44
- stdin.puts 'exit'
45
- stdin.puts 'EOF'
46
- stdin.close
47
-
48
- puts "#{stdout.read}" if debug_logging
49
-
50
- lldb_status = process_status
51
- stderr_output = stderr.read.strip
52
- end
49
+ def inject_dylib(timeout)
50
+ RunLoop.log_debug("Starting lldb injection with a timeout of #{timeout} seconds")
53
51
 
54
- pid = lldb_status.pid
55
- exit_status = lldb_status.value.exitstatus
52
+ script_path = write_script
56
53
 
57
- if stderr_output == ''
58
- if debug_logging
59
- puts "lldb '#{pid}' exited with value '#{exit_status}'."
60
- puts "Took #{Time.now-lldb_start_time} for lldb to inject calabash dylib."
61
- end
62
- else
63
- puts "#{stderr_output}"
64
- if debug_logging
65
- puts "lldb '#{pid}' exited with value '#{exit_status}'."
66
- puts "lldb tried for #{Time.now-lldb_start_time} to inject calabash dylib before giving up."
67
- end
68
- end
54
+ start = Time.now
69
55
 
70
- stderr_output == ''
71
- end
56
+ options = {
57
+ :timeout => timeout,
58
+ :log_cmd => true
59
+ }
72
60
 
73
- def inject_dylib_with_timeout(timeout)
61
+ hash = nil
74
62
  success = false
75
- Timeout.timeout(timeout) do
76
- success = inject_dylib
63
+ begin
64
+ hash = xcrun.exec(["lldb", "--no-lldbinit", "--source", script_path], options)
65
+ pid = hash[:pid]
66
+ exit_status = hash[:exit_status]
67
+ success = exit_status == 0
68
+
69
+ RunLoop.log_debug("lldb '#{pid}' exited with value '#{exit_status}'.")
70
+
71
+ success = exit_status == 0
72
+ elapsed = Time.now - start
73
+
74
+ if success
75
+ RunLoop.log_debug("Took #{elapsed} seconds for lldb to inject calabash dylib.")
76
+ else
77
+ RunLoop.log_debug("Could not inject dylib after #{elapsed} seconds.")
78
+ if hash[:out]
79
+ hash[:out].split("\n").each do |line|
80
+ RunLoop.log_debug(line)
81
+ end
82
+ else
83
+ RunLoop.log_debug("lldb returned no output to stdout or stderr")
84
+ end
85
+ end
86
+ rescue RunLoop::Xcrun::TimeoutError
87
+ elapsed = Time.now - start
88
+ RunLoop.log_debug("lldb tried for #{elapsed} seconds to inject calabash dylib before giving up.")
77
89
  end
90
+
78
91
  success
79
92
  end
80
93
 
81
94
  def retriable_inject_dylib(options={})
82
- default_options = {:tries => 3,
83
- :interval => 10,
84
- :timeout => 10}
85
- merged_options = default_options.merge(options)
86
-
87
- debug_logging = RunLoop::Environment.debug?
88
-
89
- on_retry = Proc.new do |_, try, elapsed_time, next_interval|
90
- if debug_logging
91
- # Retriable 2.0
92
- if elapsed_time && next_interval
93
- puts "LLDB: attempt #{try} failed in '#{elapsed_time}'; will retry in '#{next_interval}'"
94
- else
95
- puts "LLDB: attempt #{try} failed; will retry in #{merged_options[:interval]}"
96
- end
97
- end
98
- RunLoop::LLDB.kill_lldb_processes
99
- RunLoop::ProcessWaiter.new('lldb').wait_for_none
100
- end
95
+ merged_options = RETRY_OPTIONS.merge(options)
101
96
 
102
97
  tries = merged_options[:tries]
98
+ timeout = merged_options[:timeout]
103
99
  interval = merged_options[:interval]
104
- retry_opts = RunLoop::RetryOpts.tries_and_interval(tries, interval, {:on_retry => on_retry})
105
100
 
106
- # For some reason, :timeout does not work here;
107
- # the lldb process can hang indefinitely.
108
- Retriable.retriable(retry_opts) do
109
- unless inject_dylib_with_timeout merged_options[:timeout]
110
- raise RuntimeError, "Could not inject dylib"
111
- end
101
+ success = false
102
+
103
+ tries.times do
104
+
105
+ success = inject_dylib(timeout)
106
+ break if success
107
+
108
+ sleep(interval)
109
+ end
110
+
111
+ if !success
112
+ raise RuntimeError, "Could not inject dylib"
113
+ end
114
+ success
115
+ end
116
+
117
+ private
118
+
119
+ def write_script
120
+ script = File.join(DotDir.directory, "inject-dylib.lldb")
121
+
122
+ if File.exist?(script)
123
+ FileUtils.rm_rf(script)
124
+ end
125
+
126
+ File.open(script, "w") do |file|
127
+ file.write("process attach -n \"#{process_name}\"\n")
128
+ file.write("expr (void*)dlopen(\"#{dylib_path}\", 0x2)\n")
129
+ file.write("detach\n")
130
+ file.write("exit\n")
112
131
  end
113
- true
132
+
133
+ script
114
134
  end
115
135
  end
116
136
  end
@@ -80,32 +80,47 @@ module RunLoop
80
80
  end
81
81
  end
82
82
 
83
- # Returns the value of CAL_SIM_POST_LAUNCH_WAIT
83
+ # Returns true if running in Jenkins CI
84
84
  #
85
- # Controls how long to wait _after_ the simulator is opened.
86
- #
87
- # The default wait time is 1.0. This was arrived at through testing.
85
+ # Checks the value of JENKINS_HOME
86
+ def self.jenkins?
87
+ value = ENV["JENKINS_HOME"]
88
+ return value && value != ''
89
+ end
90
+
91
+ # Returns true if running in Travis CI
88
92
  #
89
- # In CoreSimulator environments, the iOS Simulator starts many async
90
- # processes that must be allowed to finish before we start operating on the
91
- # simulator. Until we find the right combination of processes to wait for,
92
- # this variable will give us the opportunity to control how long we wait.
93
+ # Checks the value of TRAVIS
94
+ def self.travis?
95
+ value = ENV["TRAVIS"]
96
+ return value && value != ''
97
+ end
98
+
99
+ # Returns true if running in Circle CI
93
100
  #
94
- # Essential for managed envs like Travis + Jenkins and on slower machines.
95
- def self.sim_post_launch_wait
96
- value = ENV['CAL_SIM_POST_LAUNCH_WAIT']
97
- float = nil
98
- begin
99
- float = value.to_f
100
- rescue NoMethodError => _
101
+ # Checks the value of CIRCLECI
102
+ def self.circle_ci?
103
+ value = ENV["CIRCLECI"]
104
+ return value && value != ''
105
+ end
101
106
 
102
- end
107
+ # Returns true if running in Teamcity
108
+ #
109
+ # Checks the value of TEAMCITY_PROJECT_NAME
110
+ def self.teamcity?
111
+ value = ENV["TEAMCITY_PROJECT_NAME"]
112
+ return value && value != ''
113
+ end
103
114
 
104
- if float.nil? || float == 0.0
105
- nil
106
- else
107
- float
108
- end
115
+ # Returns true if running in a CI environment
116
+ def self.ci?
117
+ [
118
+ self.ci_var_defined?,
119
+ self.travis?,
120
+ self.jenkins?,
121
+ self.circle_ci?,
122
+ self.teamcity?
123
+ ].any?
109
124
  end
110
125
 
111
126
  # !@visibility private
@@ -124,5 +139,14 @@ module RunLoop
124
139
  block.call
125
140
  end
126
141
  end
142
+
143
+ private
144
+
145
+ # !@visibility private
146
+ def self.ci_var_defined?
147
+ value = ENV["CI"]
148
+ return value && value != ''
149
+ end
127
150
  end
128
151
  end
152
+