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.
- 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
|
+
|