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.
- checksums.yaml +4 -4
- data/lib/run_loop.rb +1 -7
- data/lib/run_loop/cli/simctl.rb +6 -2
- data/lib/run_loop/core.rb +19 -49
- data/lib/run_loop/core_simulator.rb +203 -51
- data/lib/run_loop/device.rb +48 -16
- data/lib/run_loop/directory.rb +40 -16
- data/lib/run_loop/dylib_injector.rb +88 -68
- data/lib/run_loop/environment.rb +45 -21
- data/lib/run_loop/instruments.rb +2 -15
- data/lib/run_loop/lldb.rb +3 -4
- data/lib/run_loop/process_waiter.rb +4 -10
- data/lib/run_loop/sim_control.rb +19 -38
- data/lib/run_loop/version.rb +1 -1
- data/lib/run_loop/xcrun.rb +32 -69
- data/scripts/calabash_script_uia.js +5337 -5328
- data/scripts/run_loop_fast_uia.js +3 -1
- data/scripts/run_loop_host.js +3 -1
- data/scripts/run_loop_shared_element.js +3 -1
- metadata +17 -25
- data/lib/run_loop/patches/retriable.rb +0 -45
- data/lib/run_loop/xctools.rb +0 -329
data/lib/run_loop/device.rb
CHANGED
@@ -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
|
-
# @
|
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 =
|
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(
|
319
|
-
|
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
|
data/lib/run_loop/directory.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
55
|
-
exit_status = lldb_status.value.exitstatus
|
52
|
+
script_path = write_script
|
56
53
|
|
57
|
-
|
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
|
-
|
71
|
-
|
56
|
+
options = {
|
57
|
+
:timeout => timeout,
|
58
|
+
:log_cmd => true
|
59
|
+
}
|
72
60
|
|
73
|
-
|
61
|
+
hash = nil
|
74
62
|
success = false
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
132
|
+
|
133
|
+
script
|
114
134
|
end
|
115
135
|
end
|
116
136
|
end
|
data/lib/run_loop/environment.rb
CHANGED
@@ -80,32 +80,47 @@ module RunLoop
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
|
-
# Returns
|
83
|
+
# Returns true if running in Jenkins CI
|
84
84
|
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
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
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
#
|
95
|
-
def self.
|
96
|
-
value = ENV[
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
+
|