run_loop 1.5.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+