run_loop_tcc 2.1.3
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 +7 -0
- data/LICENSE +21 -0
- data/bin/run-loop +19 -0
- data/lib/run_loop/abstract.rb +18 -0
- data/lib/run_loop/app.rb +372 -0
- data/lib/run_loop/cache/cache.rb +68 -0
- data/lib/run_loop/cli/cli.rb +48 -0
- data/lib/run_loop/cli/codesign.rb +24 -0
- data/lib/run_loop/cli/errors.rb +11 -0
- data/lib/run_loop/cli/instruments.rb +160 -0
- data/lib/run_loop/cli/locale.rb +31 -0
- data/lib/run_loop/cli/simctl.rb +257 -0
- data/lib/run_loop/cli/tcc.rb +139 -0
- data/lib/run_loop/codesign.rb +76 -0
- data/lib/run_loop/core.rb +902 -0
- data/lib/run_loop/core_simulator.rb +960 -0
- data/lib/run_loop/detect_aut/detect.rb +185 -0
- data/lib/run_loop/detect_aut/errors.rb +126 -0
- data/lib/run_loop/detect_aut/xamarin_studio.rb +46 -0
- data/lib/run_loop/detect_aut/xcode.rb +157 -0
- data/lib/run_loop/device.rb +722 -0
- data/lib/run_loop/device_agent/app/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/bin/xctestctl +0 -0
- data/lib/run_loop/device_agent/cbxrunner.rb +156 -0
- data/lib/run_loop/device_agent/frameworks/Frameworks.zip +0 -0
- data/lib/run_loop/device_agent/frameworks.rb +65 -0
- data/lib/run_loop/device_agent/ipa/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/launcher.rb +51 -0
- data/lib/run_loop/device_agent/xcodebuild.rb +91 -0
- data/lib/run_loop/device_agent/xctestctl.rb +109 -0
- data/lib/run_loop/directory.rb +179 -0
- data/lib/run_loop/dnssd.rb +148 -0
- data/lib/run_loop/dot_dir.rb +87 -0
- data/lib/run_loop/dylib_injector.rb +145 -0
- data/lib/run_loop/encoding.rb +56 -0
- data/lib/run_loop/environment.rb +361 -0
- data/lib/run_loop/fifo.rb +40 -0
- data/lib/run_loop/host_cache.rb +128 -0
- data/lib/run_loop/http/error.rb +15 -0
- data/lib/run_loop/http/request.rb +44 -0
- data/lib/run_loop/http/retriable_client.rb +166 -0
- data/lib/run_loop/http/server.rb +17 -0
- data/lib/run_loop/instruments.rb +436 -0
- data/lib/run_loop/ipa.rb +142 -0
- data/lib/run_loop/l10n.rb +93 -0
- data/lib/run_loop/language.rb +63 -0
- data/lib/run_loop/lipo.rb +132 -0
- data/lib/run_loop/lldb.rb +52 -0
- data/lib/run_loop/locale.rb +101 -0
- data/lib/run_loop/logging.rb +111 -0
- data/lib/run_loop/otool.rb +76 -0
- data/lib/run_loop/patches/awesome_print.rb +17 -0
- data/lib/run_loop/physical_device/life_cycle.rb +268 -0
- data/lib/run_loop/plist_buddy.rb +189 -0
- data/lib/run_loop/process_terminator.rb +128 -0
- data/lib/run_loop/process_waiter.rb +117 -0
- data/lib/run_loop/regex.rb +19 -0
- data/lib/run_loop/shell.rb +103 -0
- data/lib/run_loop/sim_control.rb +1264 -0
- data/lib/run_loop/simctl.rb +275 -0
- data/lib/run_loop/sqlite.rb +61 -0
- data/lib/run_loop/strings.rb +88 -0
- data/lib/run_loop/tcc/TCC.db +0 -0
- data/lib/run_loop/tcc/tcc.rb +240 -0
- data/lib/run_loop/template.rb +61 -0
- data/lib/run_loop/version.rb +182 -0
- data/lib/run_loop/xcode.rb +318 -0
- data/lib/run_loop/xcrun.rb +107 -0
- data/lib/run_loop/xcuitest.rb +550 -0
- data/lib/run_loop.rb +230 -0
- data/plists/simctl/com.apple.UIAutomation.plist +0 -0
- data/plists/simctl/com.apple.UIAutomationPlugIn.plist +0 -0
- data/scripts/calabash_script_uia.js +28184 -0
- data/scripts/lib/json2.min.js +26 -0
- data/scripts/lib/log.js +26 -0
- data/scripts/lib/on_alert.js +224 -0
- data/scripts/read-cmd.sh +2 -0
- data/scripts/run_dismiss_location.js +89 -0
- data/scripts/run_loop_basic.js +34 -0
- data/scripts/run_loop_fast_uia.js +188 -0
- data/scripts/run_loop_host.js +117 -0
- data/scripts/run_loop_shared_element.js +125 -0
- data/scripts/timeout3 +23 -0
- data/scripts/udidetect +0 -0
- data/vendor-licenses/FBSimulatorControl.LICENSE +30 -0
- data/vendor-licenses/xctestctl.LICENSE +32 -0
- metadata +443 -0
@@ -0,0 +1,166 @@
|
|
1
|
+
module RunLoop
|
2
|
+
module HTTP
|
3
|
+
require "httpclient"
|
4
|
+
|
5
|
+
# An HTTP client that retries its connection on errors and can time out.
|
6
|
+
# @!visibility private
|
7
|
+
class RetriableClient
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
RETRY_ON =
|
12
|
+
[
|
13
|
+
# The connection, request, or response timed out
|
14
|
+
#HTTPClient::TimeoutError,
|
15
|
+
# The address is not found. Useful for polling.
|
16
|
+
SocketError,
|
17
|
+
# The proxy could not connect to the server (Android)
|
18
|
+
# or the server is not running (iOS)
|
19
|
+
HTTPClient::KeepAliveDisconnected,
|
20
|
+
# No proxy has been set up (Android)
|
21
|
+
Errno::ECONNREFUSED,
|
22
|
+
# The server sent a partial response
|
23
|
+
#Errno::ECONNRESET,
|
24
|
+
# Client sent TCP reset (RST) before server has accepted the
|
25
|
+
# connection requested by client.
|
26
|
+
Errno::ECONNABORTED,
|
27
|
+
# The foreign function call call timed out
|
28
|
+
#Errno::ETIMEDOUT
|
29
|
+
]
|
30
|
+
|
31
|
+
# @!visibility private
|
32
|
+
HEADER =
|
33
|
+
{
|
34
|
+
'Content-Type' => 'application/json;charset=utf-8'
|
35
|
+
}
|
36
|
+
|
37
|
+
# Creates a new retriable client.
|
38
|
+
#
|
39
|
+
# This initializer takes multiple options. If the option is not
|
40
|
+
# documented, it should be considered _private_. You use undocumented
|
41
|
+
# options at your own risk.
|
42
|
+
#
|
43
|
+
# @param [RunLoop::HTTP::Server] server The server to make the HTTP request
|
44
|
+
# on.
|
45
|
+
# @param [Hash] options Control the retry, timeout, and interval.
|
46
|
+
# @option options [Number] :retries (5) How often to retry.
|
47
|
+
# @option options [Number] :timeout (5) How long to wait for a response
|
48
|
+
# before timing out.
|
49
|
+
# @option options [Number] :interval (0.5) How long to sleep between
|
50
|
+
# retries.
|
51
|
+
def initialize(server, options = {})
|
52
|
+
@client = options[:client] || ::HTTPClient.new
|
53
|
+
@server = server
|
54
|
+
@retries = options.fetch(:retries, 5)
|
55
|
+
@timeout = options.fetch(:timeout, 5)
|
56
|
+
@interval = options.fetch(:interval, 0.5)
|
57
|
+
@on_error = {}
|
58
|
+
end
|
59
|
+
|
60
|
+
# @!visibility private
|
61
|
+
def on_error(type, &block)
|
62
|
+
@on_error[type] = block
|
63
|
+
end
|
64
|
+
|
65
|
+
# @!visibility private
|
66
|
+
def change_server(new_server)
|
67
|
+
@server = new_server
|
68
|
+
end
|
69
|
+
|
70
|
+
# Make an HTTP get request.
|
71
|
+
#
|
72
|
+
# This method takes multiple options. If the option is not documented,
|
73
|
+
# it should be considered _private_. You use undocumented options at
|
74
|
+
# your own risk.
|
75
|
+
#
|
76
|
+
# @param [RunLoop::HTTP::Request] request The request.
|
77
|
+
# @param [Hash] options Control the retry, timeout, and interval.
|
78
|
+
# @option options [Number] :retries (5) How often to retry.
|
79
|
+
# @option options [Number] :timeout (5) How long to wait for a response
|
80
|
+
# before timing out.
|
81
|
+
# @option options [Number] :interval (0.5) How long to sleep between
|
82
|
+
# retries.
|
83
|
+
def get(request, options={})
|
84
|
+
request(request, :get, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Make an HTTP post request.
|
88
|
+
#
|
89
|
+
# This method takes multiple options. If the option is not documented,
|
90
|
+
# it should be considered _private_. You use undocumented options at
|
91
|
+
# your own risk.
|
92
|
+
#
|
93
|
+
# @param [RunLoop::HTTP::Request] request The request.
|
94
|
+
# @param [Hash] options Control the retry, timeout, and interval.
|
95
|
+
# @option options [Number] :retries (5) How often to retry.
|
96
|
+
# @option options [Number] :timeout (5) How long to wait for a response
|
97
|
+
# before timing out.
|
98
|
+
# @option options [Number] :interval (0.5) How long to sleep between
|
99
|
+
# retries.
|
100
|
+
def post(request, options={})
|
101
|
+
request(request, :post, options)
|
102
|
+
end
|
103
|
+
|
104
|
+
def delete(request, options={})
|
105
|
+
request(request, :delete, options)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def request(request, request_method, options={})
|
111
|
+
retries = options.fetch(:retries, @retries)
|
112
|
+
timeout = options.fetch(:timeout, @timeout)
|
113
|
+
interval = options.fetch(:interval, @interval)
|
114
|
+
header = options.fetch(:header, HEADER)
|
115
|
+
|
116
|
+
RunLoop.log_debug("HTTP: #{@server.endpoint + request.route} #{options}")
|
117
|
+
|
118
|
+
start_time = Time.now
|
119
|
+
last_error = nil
|
120
|
+
|
121
|
+
client = @client.dup
|
122
|
+
client.receive_timeout = timeout
|
123
|
+
|
124
|
+
retries.times do |i|
|
125
|
+
first_try = i == 0
|
126
|
+
|
127
|
+
# Subtract the aggregate time we've spent thus far to make sure we're
|
128
|
+
# not exceeding the request timeout across retries.
|
129
|
+
time_diff = start_time + timeout - Time.now
|
130
|
+
|
131
|
+
if time_diff <= 0
|
132
|
+
raise HTTP::Error, 'Timeout exceeded'
|
133
|
+
end
|
134
|
+
|
135
|
+
client.receive_timeout = [time_diff, client.receive_timeout].min
|
136
|
+
|
137
|
+
begin
|
138
|
+
return client.send(request_method, @server.endpoint + request.route,
|
139
|
+
request.params, header)
|
140
|
+
rescue *RETRY_ON => e
|
141
|
+
#RunLoop.log_debug("Rescued http error: #{e}")
|
142
|
+
|
143
|
+
if first_try
|
144
|
+
if @on_error[e.class]
|
145
|
+
@on_error[e.class].call(@server)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
last_error = e
|
150
|
+
sleep interval
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# We should raise helpful messages
|
155
|
+
if last_error.is_a?(HTTPClient::KeepAliveDisconnected)
|
156
|
+
raise HTTP::Error, "#{last_error}: It is likely your server has crashed."
|
157
|
+
elsif last_error.is_a?(SocketError)
|
158
|
+
raise HTTP::Error, "#{last_error}: Did your server start and is it on the same network?"
|
159
|
+
end
|
160
|
+
|
161
|
+
raise HTTP::Error, last_error
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RunLoop
|
2
|
+
module HTTP
|
3
|
+
|
4
|
+
# A representation of the RunLoop test server.
|
5
|
+
# @!visibility private
|
6
|
+
class Server
|
7
|
+
attr_reader :endpoint
|
8
|
+
|
9
|
+
# @param [URI] endpoint The endpoint to reach the test server.
|
10
|
+
# running on the device. The port should be included in the URI.
|
11
|
+
def initialize(endpoint)
|
12
|
+
@endpoint = endpoint
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,436 @@
|
|
1
|
+
module RunLoop
|
2
|
+
|
3
|
+
# A class for interacting with the instruments command-line tool
|
4
|
+
#
|
5
|
+
# @note All instruments commands are run in the context of `xcrun`.
|
6
|
+
class Instruments
|
7
|
+
|
8
|
+
include RunLoop::Regex
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
#
|
12
|
+
# Rotates xrtmp__ directories in /Library/Caches/com.apple.dt.instruments
|
13
|
+
# keeping the last 5. On CI systems these can be hundreds of gigabytes.
|
14
|
+
def self.rotate_cache_directories
|
15
|
+
|
16
|
+
# Never run on the XTC
|
17
|
+
return :xtc if RunLoop::Environment.xtc?
|
18
|
+
|
19
|
+
cache = self.library_cache_dir
|
20
|
+
|
21
|
+
# If the directory does not exist, do nothing.
|
22
|
+
return :no_cache if !cache || !File.exist?(cache)
|
23
|
+
|
24
|
+
start = Time.now
|
25
|
+
|
26
|
+
glob = "#{cache}/xrtmp__*"
|
27
|
+
|
28
|
+
RunLoop.log_debug("Searching for instruments caches with glob: #{glob}")
|
29
|
+
|
30
|
+
directories = Dir.glob(glob).select do |path|
|
31
|
+
File.directory?(path)
|
32
|
+
end
|
33
|
+
|
34
|
+
log_progress = false
|
35
|
+
if directories.count > 25
|
36
|
+
RunLoop.log_info2("Found #{directories.count} instruments caches: ~#{20 * directories.count} Mb")
|
37
|
+
RunLoop.log_info2("Deleting them could take a long time.")
|
38
|
+
RunLoop.log_info2("This delay will only happen once!")
|
39
|
+
RunLoop.log_info2("Please be patient and allow the directories to be deleted")
|
40
|
+
log_progress = true
|
41
|
+
else
|
42
|
+
RunLoop.log_debug("Found #{directories.count} instruments caches")
|
43
|
+
end
|
44
|
+
|
45
|
+
if log_progress
|
46
|
+
RunLoop.log_info2("Sorting instruments caches by modification time...")
|
47
|
+
end
|
48
|
+
|
49
|
+
oldest_first = directories.sort_by { |f| File.mtime(f) }
|
50
|
+
|
51
|
+
oldest_first.pop(5)
|
52
|
+
|
53
|
+
if log_progress
|
54
|
+
RunLoop.log_info2("Will delete #{oldest_first.count} instruments caches...")
|
55
|
+
else
|
56
|
+
RunLoop.log_debug("Will delete #{oldest_first.count} instruments caches")
|
57
|
+
end
|
58
|
+
|
59
|
+
oldest_first.each do |path|
|
60
|
+
FileUtils.rm_rf(path)
|
61
|
+
if log_progress
|
62
|
+
printf "."
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
elapsed = Time.now - start
|
67
|
+
|
68
|
+
if log_progress
|
69
|
+
puts ""
|
70
|
+
RunLoop.log_info2("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
|
71
|
+
else
|
72
|
+
RunLoop.log_debug("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :xcode
|
78
|
+
|
79
|
+
def pbuddy
|
80
|
+
@pbuddy ||= RunLoop::PlistBuddy.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def xcode
|
84
|
+
@xcode ||= RunLoop::Xcode.new
|
85
|
+
end
|
86
|
+
|
87
|
+
def xcrun
|
88
|
+
RunLoop::Xcrun.new
|
89
|
+
end
|
90
|
+
|
91
|
+
# @!visibility private
|
92
|
+
def to_s
|
93
|
+
"#<Instruments #{version.to_s}>"
|
94
|
+
end
|
95
|
+
|
96
|
+
# @!visibility private
|
97
|
+
def inspect
|
98
|
+
to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns an Array of instruments process ids.
|
102
|
+
#
|
103
|
+
# @note The `block` parameter is included for legacy API and will be
|
104
|
+
# deprecated. Replace your existing calls with with .each or .map. The
|
105
|
+
# block argument makes this method hard to mock.
|
106
|
+
# @return [Array<Integer>] An array of instruments process ids.
|
107
|
+
def instruments_pids(&block)
|
108
|
+
pids = pids_from_ps_output
|
109
|
+
if block_given?
|
110
|
+
pids.each do |pid|
|
111
|
+
block.call(pid)
|
112
|
+
end
|
113
|
+
else
|
114
|
+
pids
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Are there any instruments processes running?
|
119
|
+
# @return [Boolean] True if there is are any instruments processes running.
|
120
|
+
def instruments_running?
|
121
|
+
instruments_pids.count > 0
|
122
|
+
end
|
123
|
+
|
124
|
+
# Send a kill signal to any running `instruments` processes.
|
125
|
+
#
|
126
|
+
# Only one instruments process can be running at any one time.
|
127
|
+
def kill_instruments(_=nil)
|
128
|
+
instruments_pids.each do |pid|
|
129
|
+
terminator = RunLoop::ProcessTerminator.new(pid, "QUIT", "instruments")
|
130
|
+
unless terminator.kill_process
|
131
|
+
terminator = RunLoop::ProcessTerminator.new(pid, "KILL", "instruments")
|
132
|
+
terminator.kill_process
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Is the Instruments.app running?
|
138
|
+
#
|
139
|
+
# If the Instruments.app is running, the instruments command line tool
|
140
|
+
# cannot take control of applications.
|
141
|
+
def instruments_app_running?
|
142
|
+
ps_output = `ps x -o pid,comm | grep Instruments.app | grep -v grep`.strip
|
143
|
+
if ps_output[/Instruments\.app/, 0]
|
144
|
+
true
|
145
|
+
else
|
146
|
+
false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Spawn a new instruments process in the context of `xcrun` and detach.
|
151
|
+
#
|
152
|
+
# @param [String] automation_template The template instruments will use when
|
153
|
+
# launching the application.
|
154
|
+
# @param [Hash] options The launch options.
|
155
|
+
# @param [String] log_file The file to log to.
|
156
|
+
# @return [Integer] Returns the process id of the instruments process.
|
157
|
+
# @todo Do I need to enumerate the launch options in the docs?
|
158
|
+
# @todo Should this raise errors?
|
159
|
+
# @todo Is this jruby compatible?
|
160
|
+
def spawn(automation_template, options, log_file)
|
161
|
+
splat_args = spawn_arguments(automation_template, options)
|
162
|
+
logger = options[:logger]
|
163
|
+
RunLoop::Logging.log_debug(logger, "xcrun #{splat_args.join(' ')} >& #{log_file}")
|
164
|
+
pid = Process.spawn('xcrun', *splat_args, {:out => log_file, :err => log_file})
|
165
|
+
Process.detach(pid)
|
166
|
+
pid.to_i
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the instruments version.
|
170
|
+
# @return [RunLoop::Version] A version object.
|
171
|
+
def version
|
172
|
+
@instruments_version ||= lambda do
|
173
|
+
version_string = pbuddy.plist_read('CFBundleShortVersionString',
|
174
|
+
path_to_instruments_app_plist)
|
175
|
+
RunLoop::Version.new(version_string)
|
176
|
+
end.call
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns an array of Instruments.app templates.
|
180
|
+
#
|
181
|
+
# Depending on the Xcode version Instruments.app templates will either be:
|
182
|
+
#
|
183
|
+
# * A full path to the template. # Xcode 5 and Xcode > 5 betas
|
184
|
+
# * The name of a template. # Xcode >= 6 (non beta)
|
185
|
+
#
|
186
|
+
# **Maintainers!** The rules above are important and explain why we can't
|
187
|
+
# simply filter by `~= /tracetemplate/`.
|
188
|
+
#
|
189
|
+
# Templates that users have saved will always be full paths - regardless
|
190
|
+
# of the Xcode version.
|
191
|
+
#
|
192
|
+
# @return [Array<String>] Instruments.app templates.
|
193
|
+
def templates
|
194
|
+
@instruments_templates ||= lambda do
|
195
|
+
args = ['instruments', '-s', 'templates']
|
196
|
+
hash = xcrun.run_command_in_context(args, log_cmd: true)
|
197
|
+
hash[:out].chomp.split("\n").map do |elm|
|
198
|
+
stripped = elm.strip.tr('"', '')
|
199
|
+
if stripped == '' || stripped == 'Known Templates:'
|
200
|
+
nil
|
201
|
+
else
|
202
|
+
stripped
|
203
|
+
end
|
204
|
+
end.compact
|
205
|
+
end.call
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns an array of the available physical devices.
|
209
|
+
#
|
210
|
+
# @return [Array<RunLoop::Device>] All the devices will be physical
|
211
|
+
# devices.
|
212
|
+
def physical_devices
|
213
|
+
@instruments_physical_devices ||= lambda do
|
214
|
+
fetch_devices[:out].chomp.split("\n").map do |line|
|
215
|
+
udid = line[DEVICE_UDID_REGEX, 0]
|
216
|
+
if udid
|
217
|
+
version = line[VERSION_REGEX, 0]
|
218
|
+
name = line.split('(').first.strip
|
219
|
+
RunLoop::Device.new(name, version, udid)
|
220
|
+
else
|
221
|
+
nil
|
222
|
+
end
|
223
|
+
end.compact
|
224
|
+
end.call
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns an array of the available simulators.
|
228
|
+
#
|
229
|
+
# **Xcode 5.1**
|
230
|
+
# * iPad Retina - Simulator - iOS 7.1
|
231
|
+
#
|
232
|
+
# **Xcode 6**
|
233
|
+
# * iPad Retina (8.3 Simulator) [EA79555F-ADB4-4D75-930C-A745EAC8FA8B]
|
234
|
+
#
|
235
|
+
# **Xcode 7**
|
236
|
+
# * iPhone 6 (9.0) [3EDC9C6E-3096-48BF-BCEC-7A5CAF8AA706]
|
237
|
+
# * iPhone 6 (9.0) + Apple Watch - 38mm (2.0) [EE3C200C-69BA-4816-A087-0457C5FCEDA0]
|
238
|
+
#
|
239
|
+
# @return [Array<RunLoop::Device>] All the devices will be simulators.
|
240
|
+
def simulators
|
241
|
+
@instruments_simulators ||= lambda do
|
242
|
+
fetch_devices[:out].chomp.split("\n").map do |line|
|
243
|
+
stripped = line.strip
|
244
|
+
if line_is_simulator?(stripped) &&
|
245
|
+
!line_is_simulator_paired_with_watch?(stripped) &&
|
246
|
+
!line_is_apple_tv?(stripped)
|
247
|
+
|
248
|
+
version = stripped[VERSION_REGEX, 0]
|
249
|
+
|
250
|
+
if line_is_xcode5_simulator?(stripped)
|
251
|
+
name = line
|
252
|
+
udid = line
|
253
|
+
else
|
254
|
+
name = stripped.split('(').first.strip
|
255
|
+
udid = line[CORE_SIMULATOR_UDID_REGEX, 0]
|
256
|
+
end
|
257
|
+
|
258
|
+
RunLoop::Device.new(name, version, udid)
|
259
|
+
else
|
260
|
+
nil
|
261
|
+
end
|
262
|
+
end.compact
|
263
|
+
end.call
|
264
|
+
end
|
265
|
+
|
266
|
+
private
|
267
|
+
|
268
|
+
# @!visibility private
|
269
|
+
def fetch_devices
|
270
|
+
@device_hash ||= lambda do
|
271
|
+
args = ['instruments', '-s', 'devices']
|
272
|
+
xcrun.run_command_in_context(args, log_cmd: true)
|
273
|
+
end.call
|
274
|
+
end
|
275
|
+
|
276
|
+
# @!visibility private
|
277
|
+
#
|
278
|
+
# ```
|
279
|
+
# $ ps x -o pid,command | grep -v grep | grep instruments
|
280
|
+
# 98081 sh -c xcrun instruments -w "43be3f89d9587e9468c24672777ff6241bd91124" < args >
|
281
|
+
# 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args >
|
282
|
+
# ```
|
283
|
+
#
|
284
|
+
# When run from run-loop (via rspec), expect this:
|
285
|
+
#
|
286
|
+
# ```
|
287
|
+
# $ ps x -o pid,command | grep -v grep | grep instruments
|
288
|
+
# 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args >
|
289
|
+
# ```
|
290
|
+
INSTRUMENTS_FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep instruments'
|
291
|
+
|
292
|
+
# @!visibility private
|
293
|
+
# Parses the run-loop options hash into an array of arguments that can be
|
294
|
+
# passed to `Process.spawn` to launch instruments.
|
295
|
+
def spawn_arguments(automation_template, options)
|
296
|
+
array = ['instruments']
|
297
|
+
array << '-w'
|
298
|
+
|
299
|
+
array << options[:udid]
|
300
|
+
|
301
|
+
trace = options[:results_dir_trace]
|
302
|
+
if trace
|
303
|
+
array << '-D'
|
304
|
+
array << trace
|
305
|
+
end
|
306
|
+
|
307
|
+
array << '-t'
|
308
|
+
array << automation_template
|
309
|
+
|
310
|
+
array << options[:bundle_id]
|
311
|
+
|
312
|
+
{
|
313
|
+
'UIARESULTSPATH' => options[:results_dir],
|
314
|
+
'UIASCRIPT' => options[:script]
|
315
|
+
}.each do |key, value|
|
316
|
+
array << '-e'
|
317
|
+
array << key
|
318
|
+
array << value
|
319
|
+
end
|
320
|
+
array + options.fetch(:args, [])
|
321
|
+
end
|
322
|
+
|
323
|
+
# @!visibility private
|
324
|
+
#
|
325
|
+
# Executes `ps_cmd` to find instruments processes and returns the result.
|
326
|
+
#
|
327
|
+
# @param [String] ps_cmd The Unix ps command to execute to find instruments
|
328
|
+
# processes.
|
329
|
+
# @return [String] A ps-style list of process details. The details returned
|
330
|
+
# are controlled by the `ps_cmd`.
|
331
|
+
def ps_for_instruments(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
|
332
|
+
`#{ps_cmd}`.strip
|
333
|
+
end
|
334
|
+
|
335
|
+
# @!visibility private
|
336
|
+
# Is the process described an instruments process?
|
337
|
+
#
|
338
|
+
# @param [String] ps_details Details about a process as returned by `ps`
|
339
|
+
# @return [Boolean] True if the details describe an instruments process.
|
340
|
+
def is_instruments_process?(ps_details)
|
341
|
+
return false if ps_details.nil?
|
342
|
+
ps_details[/\/usr\/bin\/instruments/, 0] != nil
|
343
|
+
end
|
344
|
+
|
345
|
+
# @!visibility private
|
346
|
+
# Extracts an Array of integer process ids from the output of executing
|
347
|
+
# the Unix `ps_cmd`.
|
348
|
+
#
|
349
|
+
# @param [String] ps_cmd The Unix `ps` command used to find instruments
|
350
|
+
# processes.
|
351
|
+
# @return [Array<Integer>] An array of integer pids for instruments
|
352
|
+
# processes. Returns an empty list if no instruments process are found.
|
353
|
+
def pids_from_ps_output(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
|
354
|
+
ps_output = ps_for_instruments(ps_cmd)
|
355
|
+
lines = ps_output.lines("\n").map { |line| line.strip }
|
356
|
+
lines.map do |line|
|
357
|
+
tokens = line.strip.split(' ').map { |token| token.strip }
|
358
|
+
pid = tokens.fetch(0, nil)
|
359
|
+
process_description = tokens[1..-1].join(' ')
|
360
|
+
if is_instruments_process? process_description
|
361
|
+
pid.to_i
|
362
|
+
else
|
363
|
+
nil
|
364
|
+
end
|
365
|
+
end.compact.sort
|
366
|
+
end
|
367
|
+
|
368
|
+
# @!visibility private
|
369
|
+
#
|
370
|
+
# Execute an instruments command.
|
371
|
+
# @param [Array] args An array of arguments
|
372
|
+
def execute_command(args)
|
373
|
+
Open3.popen3('xcrun', 'instruments', *args) do |_, stdout, stderr, process_status|
|
374
|
+
yield stdout, stderr, process_status
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# @!visibility private
|
379
|
+
def line_is_simulator?(line)
|
380
|
+
line_is_core_simulator?(line) || line_is_xcode5_simulator?(line)
|
381
|
+
end
|
382
|
+
|
383
|
+
# @!visibility private
|
384
|
+
def line_is_xcode5_simulator?(line)
|
385
|
+
!line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Simulator/, 0]
|
386
|
+
end
|
387
|
+
|
388
|
+
# @!visibility private
|
389
|
+
def line_is_core_simulator?(line)
|
390
|
+
return nil if !line_has_a_version?(line)
|
391
|
+
|
392
|
+
line[CORE_SIMULATOR_UDID_REGEX, 0]
|
393
|
+
end
|
394
|
+
|
395
|
+
# @!visibility private
|
396
|
+
def line_has_a_version?(line)
|
397
|
+
line[VERSION_REGEX, 0]
|
398
|
+
end
|
399
|
+
|
400
|
+
# @!visibility private
|
401
|
+
def line_is_simulator_paired_with_watch?(line)
|
402
|
+
line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Apple Watch/, 0]
|
403
|
+
end
|
404
|
+
|
405
|
+
# @!visibility private
|
406
|
+
def line_is_apple_tv?(line)
|
407
|
+
line[/Apple TV/, 0]
|
408
|
+
end
|
409
|
+
|
410
|
+
# @!visibility private
|
411
|
+
def path_to_instruments_app_plist
|
412
|
+
@path_to_instruments_app_plist ||=
|
413
|
+
File.expand_path(File.join(xcode.developer_dir,
|
414
|
+
'..',
|
415
|
+
'Applications',
|
416
|
+
'Instruments.app',
|
417
|
+
'Contents',
|
418
|
+
'Info.plist'))
|
419
|
+
end
|
420
|
+
|
421
|
+
# @!visibility private
|
422
|
+
#
|
423
|
+
# Instruments caches files in this directory and it can become quite large
|
424
|
+
# over time; particularly on CI system.
|
425
|
+
def self.library_cache_dir
|
426
|
+
path = "/Library/Caches/com.apple.dt.instruments"
|
427
|
+
|
428
|
+
if File.exist?(path)
|
429
|
+
path
|
430
|
+
else
|
431
|
+
nil
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|