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,960 @@
|
|
1
|
+
# A class to manage interactions with CoreSimulators.
|
2
|
+
class RunLoop::CoreSimulator
|
3
|
+
|
4
|
+
# These options control various aspects of an app's life cycle on the iOS
|
5
|
+
# Simulator.
|
6
|
+
#
|
7
|
+
# You can override these values if they do not work in your environment.
|
8
|
+
#
|
9
|
+
# For cucumber users, the best place to override would be in your
|
10
|
+
# features/support/env.rb.
|
11
|
+
#
|
12
|
+
# For example:
|
13
|
+
#
|
14
|
+
# RunLoop::CoreSimulator::DEFAULT_OPTIONS[:install_app_timeout] = 60
|
15
|
+
DEFAULT_OPTIONS = {
|
16
|
+
# In most cases 30 seconds is a reasonable amount of time to wait for an
|
17
|
+
# install. When testing larger apps, on slow machines, or in CI, this
|
18
|
+
# value may need to be higher. 120 is the default for CI.
|
19
|
+
:install_app_timeout => RunLoop::Environment.ci? ? 120 : 30,
|
20
|
+
:uninstall_app_timeout => RunLoop::Environment.ci? ? 120 : 30,
|
21
|
+
:launch_app_timeout => RunLoop::Environment.ci? ? 120 : 30,
|
22
|
+
:wait_for_state_timeout => RunLoop::Environment.ci? ? 120 : 30
|
23
|
+
}
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
# This should not be overridden
|
27
|
+
WAIT_FOR_SIMULATOR_STATE_INTERVAL = 0.1
|
28
|
+
|
29
|
+
# @!visibility private
|
30
|
+
@@simulator_pid = nil
|
31
|
+
|
32
|
+
# @!visibility private
|
33
|
+
attr_reader :app
|
34
|
+
|
35
|
+
# @!visibility private
|
36
|
+
attr_reader :device
|
37
|
+
|
38
|
+
# @!visibility private
|
39
|
+
attr_reader :pbuddy
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
attr_reader :xcode
|
43
|
+
|
44
|
+
# @!visibility private
|
45
|
+
attr_reader :xcrun
|
46
|
+
|
47
|
+
# @!visibility private
|
48
|
+
METADATA_PLIST = '.com.apple.mobile_container_manager.metadata.plist'
|
49
|
+
|
50
|
+
# @!visibility private
|
51
|
+
CORE_SIMULATOR_DEVICE_DIR = File.join(RunLoop::Environment.user_home_directory,
|
52
|
+
"Library",
|
53
|
+
"Developer",
|
54
|
+
"CoreSimulator",
|
55
|
+
"Devices")
|
56
|
+
|
57
|
+
|
58
|
+
# @!visibility private
|
59
|
+
MANAGED_PROCESSES =
|
60
|
+
[
|
61
|
+
# This process is a daemon, and requires 'KILL' to terminate.
|
62
|
+
# Killing the process is fast, but it takes a long time to
|
63
|
+
# restart.
|
64
|
+
['com.apple.CoreSimulator.CoreSimulatorService', false],
|
65
|
+
|
66
|
+
# Probably do not need to quit this, but it is tempting to do so.
|
67
|
+
#['com.apple.CoreSimulator.SimVerificationService', false],
|
68
|
+
|
69
|
+
'SimulatorBridge',
|
70
|
+
'configd_sim',
|
71
|
+
|
72
|
+
# Does not always appear.
|
73
|
+
'CoreSimulatorBridge',
|
74
|
+
|
75
|
+
# Xcode 7
|
76
|
+
'ids_simd'
|
77
|
+
]
|
78
|
+
|
79
|
+
# @!visibility private
|
80
|
+
# Pattern:
|
81
|
+
# [ '< process name >', < send term first > ]
|
82
|
+
SIMULATOR_QUIT_PROCESSES =
|
83
|
+
[
|
84
|
+
# Xcode 7 start throwing this error.
|
85
|
+
['splashboardd', false],
|
86
|
+
|
87
|
+
# Xcode < 5.1
|
88
|
+
['iPhone Simulator.app', true],
|
89
|
+
|
90
|
+
# 7.0 < Xcode <= 6.0
|
91
|
+
['iOS Simulator.app', true],
|
92
|
+
|
93
|
+
# Xcode >= 7.0
|
94
|
+
['Simulator.app', true],
|
95
|
+
|
96
|
+
# Multiple launchd_sim processes have been causing problems. This
|
97
|
+
# is a first pass at investigating what it would mean to kill the
|
98
|
+
# launchd_sim process.
|
99
|
+
['launchd_sim', false],
|
100
|
+
|
101
|
+
# Required for XCUITest termination; the simulator hangs otherwise.
|
102
|
+
["xpcproxy", false],
|
103
|
+
|
104
|
+
# Causes crash reports on Xcode < 7.0
|
105
|
+
["apsd", true],
|
106
|
+
|
107
|
+
# assetsd instances clobber each other and are not properly
|
108
|
+
# killed when quiting the simulator.
|
109
|
+
['assetsd', false],
|
110
|
+
|
111
|
+
# iproxy is started by UITest.
|
112
|
+
['iproxy', false],
|
113
|
+
|
114
|
+
# Started by Xamarin Studio, this is the parent process of the
|
115
|
+
# processes launched by Xamarin's interaction with
|
116
|
+
# CoreSimulatorBridge.
|
117
|
+
['csproxy', false],
|
118
|
+
]
|
119
|
+
|
120
|
+
# @!visibility private
|
121
|
+
#
|
122
|
+
# Terminate CoreSimulator related processes. This processes can accumulate
|
123
|
+
# as testing proceeds and can cause instability.
|
124
|
+
def self.terminate_core_simulator_processes
|
125
|
+
|
126
|
+
self.quit_simulator
|
127
|
+
|
128
|
+
MANAGED_PROCESSES.each do |process_name|
|
129
|
+
send_term_first = false
|
130
|
+
self.term_or_kill(process_name, send_term_first)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @!visibility private
|
135
|
+
# Quit any Simulator.app or iOS Simulator.app
|
136
|
+
def self.quit_simulator
|
137
|
+
SIMULATOR_QUIT_PROCESSES.each do |process_details|
|
138
|
+
process_name = process_details[0]
|
139
|
+
send_term_first = process_details[1]
|
140
|
+
self.term_or_kill(process_name, send_term_first)
|
141
|
+
end
|
142
|
+
|
143
|
+
self.simulator_pid = nil
|
144
|
+
end
|
145
|
+
|
146
|
+
# @!visibility private
|
147
|
+
#
|
148
|
+
# Some operations, like erase, require that the simulator be
|
149
|
+
# 'Shutdown'.
|
150
|
+
#
|
151
|
+
# @param [RunLoop::Device] simulator the sim to wait for
|
152
|
+
# @param [String] target_state the state to wait for
|
153
|
+
def self.wait_for_simulator_state(simulator, target_state)
|
154
|
+
now = Time.now
|
155
|
+
timeout = DEFAULT_OPTIONS[:wait_for_state_timeout]
|
156
|
+
poll_until = now + timeout
|
157
|
+
delay = WAIT_FOR_SIMULATOR_STATE_INTERVAL
|
158
|
+
in_state = false
|
159
|
+
while Time.now < poll_until
|
160
|
+
in_state = simulator.update_simulator_state == target_state
|
161
|
+
break if in_state
|
162
|
+
sleep delay if delay != 0
|
163
|
+
end
|
164
|
+
|
165
|
+
elapsed = Time.now - now
|
166
|
+
RunLoop.log_debug("Waited for #{elapsed} seconds for device to have state: '#{target_state}'.")
|
167
|
+
|
168
|
+
unless in_state
|
169
|
+
raise "Expected '#{target_state} but found '#{simulator.state}' after waiting."
|
170
|
+
end
|
171
|
+
in_state
|
172
|
+
end
|
173
|
+
|
174
|
+
# @!visibility private
|
175
|
+
# Erase a simulator. This is the same as touching the Simulator
|
176
|
+
# "Reset Content & Settings" menu item.
|
177
|
+
#
|
178
|
+
# @param [RunLoop::Device] simulator The simulator to erase
|
179
|
+
# @param [Hash] options Control the behavior of the method.
|
180
|
+
# @option options [Numeric] :timeout (180) How long tow wait for simctl to
|
181
|
+
# shutdown and erase the simulator. The timeout is apply separately to
|
182
|
+
# each command.
|
183
|
+
#
|
184
|
+
# @raise RuntimeError If the simulator cannot be shutdown
|
185
|
+
# @raise RuntimeError If the simulator cannot be erased
|
186
|
+
# @raise ArgumentError If the simulator is a physical device
|
187
|
+
def self.erase(simulator, options={})
|
188
|
+
if simulator.physical_device?
|
189
|
+
raise ArgumentError,
|
190
|
+
"#{simulator} is a physical device. This method is only for Simulators"
|
191
|
+
end
|
192
|
+
|
193
|
+
default_options = {
|
194
|
+
:timeout => 60*3
|
195
|
+
}
|
196
|
+
|
197
|
+
merged_options = default_options.merge(options)
|
198
|
+
|
199
|
+
self.quit_simulator
|
200
|
+
|
201
|
+
xcrun = merged_options[:xcrun] || RunLoop::Xcrun.new
|
202
|
+
timeout = merged_options[:timeout]
|
203
|
+
xcrun_opts = {
|
204
|
+
:log_cmd => true,
|
205
|
+
:timeout => timeout
|
206
|
+
}
|
207
|
+
|
208
|
+
if simulator.update_simulator_state != "Shutdown"
|
209
|
+
args = ["simctl", "shutdown", simulator.udid]
|
210
|
+
xcrun.run_command_in_context(args, xcrun_opts)
|
211
|
+
begin
|
212
|
+
self.wait_for_simulator_state(simulator, "Shutdown")
|
213
|
+
rescue RuntimeError => _
|
214
|
+
raise RuntimeError, %Q{
|
215
|
+
Could not erase simulator because it could not be Shutdown.
|
216
|
+
|
217
|
+
This usually means your CoreSimulator processes need to be restarted.
|
218
|
+
|
219
|
+
You can restart the CoreSimulator processes with this command:
|
220
|
+
|
221
|
+
$ bundle exec run-loop simctl manage-processes
|
222
|
+
|
223
|
+
}
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
args = ["simctl", "erase", simulator.udid]
|
229
|
+
hash = xcrun.run_command_in_context(args, xcrun_opts)
|
230
|
+
|
231
|
+
if hash[:exit_status] != 0
|
232
|
+
raise RuntimeError, %Q{
|
233
|
+
Could not erase simulator because simctl returned this error:
|
234
|
+
|
235
|
+
#{hash[:out]}
|
236
|
+
|
237
|
+
This usually means your CoreSimulator processes need to be restarted.
|
238
|
+
|
239
|
+
You can restart the CoreSimulator processes with this command:
|
240
|
+
|
241
|
+
$ bundle exec run-loop simctl manage-processes
|
242
|
+
|
243
|
+
}
|
244
|
+
|
245
|
+
end
|
246
|
+
|
247
|
+
hash
|
248
|
+
end
|
249
|
+
|
250
|
+
# @!visibility private
|
251
|
+
#
|
252
|
+
# @param [RunLoop::Device, String] device a simulator UDID, instruments-ready
|
253
|
+
# name, or a RunLoop::Device.
|
254
|
+
#
|
255
|
+
# @param [String] locale_code a locale code
|
256
|
+
#
|
257
|
+
# @raise [ArgumentError] if no device can be found matching the UDID or
|
258
|
+
# instruments-ready name
|
259
|
+
# @raise [ArgumentError] if device is not a simulator
|
260
|
+
# @raise [ArgumentError] if locale_code is invalid
|
261
|
+
def self.set_locale(device, locale_code)
|
262
|
+
if device.is_a?(RunLoop::Device)
|
263
|
+
simulator = device
|
264
|
+
else
|
265
|
+
simulator = RunLoop::Device.device_with_identifier(device)
|
266
|
+
end
|
267
|
+
|
268
|
+
if simulator.physical_device?
|
269
|
+
raise ArgumentError,
|
270
|
+
"The locale cannot be set on physical devices"
|
271
|
+
end
|
272
|
+
|
273
|
+
self.quit_simulator
|
274
|
+
RunLoop.log_debug("Setting locale to '#{locale_code}'")
|
275
|
+
simulator.simulator_set_locale(locale_code)
|
276
|
+
end
|
277
|
+
|
278
|
+
# @!visibility private
|
279
|
+
#
|
280
|
+
# @param [RunLoop::Device, String] device a simulator UDID, instruments-ready
|
281
|
+
# name, or a RunLoop::Device
|
282
|
+
# @param [String] lang_code a language code
|
283
|
+
#
|
284
|
+
# @raise [ArgumentError] if no device can be found matching the UDID or
|
285
|
+
# instruments-ready name
|
286
|
+
# @raise [ArgumentError] if device is not a simulator
|
287
|
+
# @raise [ArgumentError] if language_code is invalid
|
288
|
+
def self.set_language(device, lang_code)
|
289
|
+
if device.is_a?(RunLoop::Device)
|
290
|
+
simulator = device
|
291
|
+
else
|
292
|
+
simulator = RunLoop::Device.device_with_identifier(device)
|
293
|
+
end
|
294
|
+
|
295
|
+
if simulator.physical_device?
|
296
|
+
raise ArgumentError,
|
297
|
+
"The language cannot be set on physical devices"
|
298
|
+
end
|
299
|
+
|
300
|
+
self.quit_simulator
|
301
|
+
RunLoop.log_debug("Setting preferred language to '#{lang_code}'")
|
302
|
+
simulator.simulator_set_language(lang_code)
|
303
|
+
end
|
304
|
+
|
305
|
+
# @!visibility private
|
306
|
+
def self.simulator_pid
|
307
|
+
@@simulator_pid
|
308
|
+
end
|
309
|
+
|
310
|
+
# @!visibility private
|
311
|
+
def self.simulator_pid=(pid)
|
312
|
+
@@simulator_pid = pid
|
313
|
+
end
|
314
|
+
|
315
|
+
# @param [RunLoop::Device] device The device.
|
316
|
+
# @param [RunLoop::App] app The application.
|
317
|
+
# @param [Hash] options Controls the behavior of this class.
|
318
|
+
# @option options :quit_sim_on_init (true) If true, quit any running
|
319
|
+
# @option options :xcode An instance of Xcode to use
|
320
|
+
# simulators in the initialize method.
|
321
|
+
def initialize(device, app, options={})
|
322
|
+
defaults = { :quit_sim_on_init => true }
|
323
|
+
merged = defaults.merge(options)
|
324
|
+
|
325
|
+
@app = app
|
326
|
+
@device = device
|
327
|
+
|
328
|
+
@xcode = merged[:xcode]
|
329
|
+
|
330
|
+
if merged[:quit_sim_on_init]
|
331
|
+
RunLoop::CoreSimulator.quit_simulator
|
332
|
+
end
|
333
|
+
|
334
|
+
# stdio.pipe - can cause problems finding the SHA of a simulator
|
335
|
+
rm_instruments_pipe
|
336
|
+
end
|
337
|
+
|
338
|
+
# @!visibility private
|
339
|
+
def pbuddy
|
340
|
+
@pbuddy ||= RunLoop::PlistBuddy.new
|
341
|
+
end
|
342
|
+
|
343
|
+
# @!visibility private
|
344
|
+
def xcode
|
345
|
+
@xcode ||= RunLoop::Xcode.new
|
346
|
+
end
|
347
|
+
|
348
|
+
# @!visibility private
|
349
|
+
def xcrun
|
350
|
+
@xcrun ||= RunLoop::Xcrun.new
|
351
|
+
end
|
352
|
+
|
353
|
+
# Launch the simulator indicated by device.
|
354
|
+
def launch_simulator
|
355
|
+
|
356
|
+
if running_simulator_pid != nil
|
357
|
+
# There is a running simulator.
|
358
|
+
|
359
|
+
# Did we launch it?
|
360
|
+
if running_simulator_pid == RunLoop::CoreSimulator.simulator_pid
|
361
|
+
# Nothing to do, we already launched the simulator.
|
362
|
+
return
|
363
|
+
else
|
364
|
+
# We did not launch this simulator; quit it.
|
365
|
+
RunLoop::CoreSimulator.quit_simulator
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
args = ['open', '-g', '-a', sim_app_path, '--args', '-CurrentDeviceUDID', device.udid]
|
370
|
+
|
371
|
+
RunLoop.log_debug("Launching #{device} with:")
|
372
|
+
RunLoop.log_unix_cmd("xcrun #{args.join(' ')}")
|
373
|
+
|
374
|
+
start_time = Time.now
|
375
|
+
|
376
|
+
pid = Process.spawn('xcrun', *args)
|
377
|
+
Process.detach(pid)
|
378
|
+
|
379
|
+
options = { :timeout => 5, :raise_on_timeout => true }
|
380
|
+
RunLoop::ProcessWaiter.new(sim_name, options).wait_for_any
|
381
|
+
|
382
|
+
device.simulator_wait_for_stable_state
|
383
|
+
|
384
|
+
elapsed = Time.now - start_time
|
385
|
+
RunLoop.log_debug("Took #{elapsed} seconds to launch the simulator")
|
386
|
+
|
387
|
+
# Keep track of the pid so we can know if we have already launched this sim.
|
388
|
+
RunLoop::CoreSimulator.simulator_pid = running_simulator_pid
|
389
|
+
|
390
|
+
true
|
391
|
+
end
|
392
|
+
|
393
|
+
# Launch the app on the simulator.
|
394
|
+
#
|
395
|
+
# 1. If the app is not installed, it is installed.
|
396
|
+
# 2. If the app is different from the app that is installed, it is installed.
|
397
|
+
def launch
|
398
|
+
install
|
399
|
+
|
400
|
+
# If the app is the same, install will not launch the simulator.
|
401
|
+
# In order to launch the app, the simulator needs to be running.
|
402
|
+
# launch_simulator ensures that the sim is launched and will not
|
403
|
+
# relaunch it.
|
404
|
+
launch_simulator
|
405
|
+
|
406
|
+
tries = RunLoop::Environment.ci? ? 5 : 3
|
407
|
+
last_error = nil
|
408
|
+
|
409
|
+
RunLoop.log_debug("Trying #{tries} times to launch #{app.bundle_identifier} on #{device}")
|
410
|
+
|
411
|
+
tries.times do |try|
|
412
|
+
# Terminates CoreSimulatorService on failures.
|
413
|
+
hash = attempt_to_launch_app_with_simctl
|
414
|
+
|
415
|
+
exit_status = hash[:exit_status]
|
416
|
+
if exit_status != 0
|
417
|
+
# Last argument is how long to sleep after an error.
|
418
|
+
last_error = handle_failed_app_launch(hash, try, tries, 0.5)
|
419
|
+
else
|
420
|
+
last_error = nil
|
421
|
+
break
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
if last_error
|
426
|
+
raise RuntimeError, %Q[Could not launch #{app.bundle_identifier} on #{device}
|
427
|
+
|
428
|
+
#{last_error}
|
429
|
+
|
430
|
+
]
|
431
|
+
end
|
432
|
+
|
433
|
+
wait_for_app_launch
|
434
|
+
end
|
435
|
+
|
436
|
+
# @!visibility private
|
437
|
+
def wait_for_app_launch
|
438
|
+
options = {
|
439
|
+
:timeout => 10,
|
440
|
+
:raise_on_timeout => true
|
441
|
+
}
|
442
|
+
RunLoop::ProcessWaiter.new(app.executable_name, options).wait_for_any
|
443
|
+
device.simulator_wait_for_stable_state
|
444
|
+
true
|
445
|
+
end
|
446
|
+
|
447
|
+
# Install the app.
|
448
|
+
#
|
449
|
+
# 1. If the app is not installed, it is installed.
|
450
|
+
# 2. Does nothing, if the app is the same as the one that is installed.
|
451
|
+
# 3. Installs the app if it is different from the installed app.
|
452
|
+
#
|
453
|
+
# The app sandbox is not touched.
|
454
|
+
def install
|
455
|
+
installed_app_bundle = installed_app_bundle_dir
|
456
|
+
|
457
|
+
# App is not installed. Use simctl interface to install.
|
458
|
+
if !installed_app_bundle
|
459
|
+
installed_app_bundle = install_app_with_simctl
|
460
|
+
else
|
461
|
+
ensure_app_same
|
462
|
+
end
|
463
|
+
|
464
|
+
installed_app_bundle
|
465
|
+
end
|
466
|
+
|
467
|
+
# Is this app installed?
|
468
|
+
def app_is_installed?
|
469
|
+
!installed_app_bundle_dir.nil?
|
470
|
+
end
|
471
|
+
|
472
|
+
# Resets the app sandbox.
|
473
|
+
#
|
474
|
+
# Does nothing if the app is not installed.
|
475
|
+
def reset_app_sandbox
|
476
|
+
return true if !app_is_installed?
|
477
|
+
|
478
|
+
RunLoop::CoreSimulator.wait_for_simulator_state(device, "Shutdown")
|
479
|
+
|
480
|
+
reset_app_sandbox_internal
|
481
|
+
end
|
482
|
+
|
483
|
+
# Uninstalls the app and clears the sandbox.
|
484
|
+
def uninstall_app_and_sandbox
|
485
|
+
return true if !app_is_installed?
|
486
|
+
|
487
|
+
launch_simulator
|
488
|
+
|
489
|
+
args = ['simctl', 'uninstall', device.udid, app.bundle_identifier]
|
490
|
+
|
491
|
+
timeout = DEFAULT_OPTIONS[:uninstall_app_timeout]
|
492
|
+
xcrun.run_command_in_context(args, log_cmd: true, timeout: timeout)
|
493
|
+
|
494
|
+
device.simulator_wait_for_stable_state
|
495
|
+
true
|
496
|
+
end
|
497
|
+
|
498
|
+
private
|
499
|
+
|
500
|
+
# @!visibility private
|
501
|
+
#
|
502
|
+
# This stdio.pipe file causes problems when checking the size and taking the
|
503
|
+
# checksum of the core simulator directory.
|
504
|
+
def rm_instruments_pipe
|
505
|
+
device_tmp_dir = File.join(device_data_dir, 'tmp')
|
506
|
+
Dir.glob("#{device_tmp_dir}/instruments_*/stdio.pipe") do |file|
|
507
|
+
if File.exist?(file)
|
508
|
+
RunLoop.log_debug("Deleting #{file}")
|
509
|
+
FileUtils.rm_rf(file)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
# Send 'TERM' then 'KILL' to allow processes to quit cleanly.
|
515
|
+
def self.term_or_kill(process_name, send_term_first)
|
516
|
+
term_options = { :timeout => 0.5 }
|
517
|
+
kill_options = { :timeout => 0.5 }
|
518
|
+
|
519
|
+
RunLoop::ProcessWaiter.new(process_name).pids.each do |pid|
|
520
|
+
killed = false
|
521
|
+
|
522
|
+
if send_term_first
|
523
|
+
term = RunLoop::ProcessTerminator.new(pid, 'TERM', process_name, term_options)
|
524
|
+
killed = term.kill_process
|
525
|
+
end
|
526
|
+
|
527
|
+
unless killed
|
528
|
+
RunLoop::ProcessTerminator.new(pid, 'KILL', process_name, kill_options)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Returns the current simulator name.
|
534
|
+
#
|
535
|
+
# @return [String] A String suitable for searching for a pid, quitting, or
|
536
|
+
# launching the current simulator.
|
537
|
+
def sim_name
|
538
|
+
@sim_name ||= lambda {
|
539
|
+
if xcode.version_gte_7?
|
540
|
+
"Simulator"
|
541
|
+
else
|
542
|
+
"iOS Simulator"
|
543
|
+
end
|
544
|
+
}.call
|
545
|
+
end
|
546
|
+
|
547
|
+
# @!visibility private
|
548
|
+
# Returns the path to the current simulator.
|
549
|
+
#
|
550
|
+
# @return [String] The path to the simulator app for the current version of
|
551
|
+
# Xcode.
|
552
|
+
def sim_app_path
|
553
|
+
@sim_app_path ||= lambda {
|
554
|
+
dev_dir = xcode.developer_dir
|
555
|
+
if xcode.version_gte_7?
|
556
|
+
"#{dev_dir}/Applications/Simulator.app"
|
557
|
+
else
|
558
|
+
"#{dev_dir}/Applications/iOS Simulator.app"
|
559
|
+
end
|
560
|
+
}.call
|
561
|
+
end
|
562
|
+
|
563
|
+
# @!visibility private
|
564
|
+
# Returns the current Simulator pid.
|
565
|
+
#
|
566
|
+
# @note Will only search for the current Xcode simulator.
|
567
|
+
#
|
568
|
+
# @return [Integer, nil] The pid as a String or nil if no process is found.
|
569
|
+
def running_simulator_pid
|
570
|
+
process_name = "MacOS/#{sim_name}"
|
571
|
+
|
572
|
+
args = ["ps", "x", "-o", "pid,command"]
|
573
|
+
hash = xcrun.run_command_in_context(args)
|
574
|
+
|
575
|
+
exit_status = hash[:exit_status]
|
576
|
+
if exit_status != 0
|
577
|
+
raise RuntimeError,
|
578
|
+
%Q{Could not find the pid of #{sim_name} with:
|
579
|
+
|
580
|
+
#{args.join(" ")}
|
581
|
+
|
582
|
+
Command exited with status #{exit_status}
|
583
|
+
Message: '#{hash[:out]}'
|
584
|
+
}
|
585
|
+
end
|
586
|
+
|
587
|
+
if hash[:out].nil? || hash[:out] == ""
|
588
|
+
raise RuntimeError,
|
589
|
+
%Q{Could not find the pid of #{sim_name} with:
|
590
|
+
|
591
|
+
#{args.join(" ")}
|
592
|
+
|
593
|
+
Command had no output
|
594
|
+
}
|
595
|
+
end
|
596
|
+
|
597
|
+
lines = hash[:out].split("\n")
|
598
|
+
|
599
|
+
match = lines.detect do |line|
|
600
|
+
line[/#{process_name}/, 0]
|
601
|
+
end
|
602
|
+
|
603
|
+
return nil if match.nil?
|
604
|
+
|
605
|
+
match.split(" ").first.to_i
|
606
|
+
end
|
607
|
+
|
608
|
+
# @!visibility private
|
609
|
+
def install_app_with_simctl
|
610
|
+
launch_simulator
|
611
|
+
|
612
|
+
args = ['simctl', 'install', device.udid, app.path]
|
613
|
+
timeout = DEFAULT_OPTIONS[:install_app_timeout]
|
614
|
+
xcrun.run_command_in_context(args, log_cmd: true, timeout: timeout)
|
615
|
+
|
616
|
+
device.simulator_wait_for_stable_state
|
617
|
+
installed_app_bundle_dir
|
618
|
+
end
|
619
|
+
|
620
|
+
# @!visibility private
|
621
|
+
def launch_app_with_simctl
|
622
|
+
args = ['simctl', 'launch', device.udid, app.bundle_identifier]
|
623
|
+
timeout = DEFAULT_OPTIONS[:launch_app_timeout]
|
624
|
+
xcrun.run_command_in_context(args, log_cmd: true, timeout: timeout)
|
625
|
+
end
|
626
|
+
|
627
|
+
# @!visibility private
|
628
|
+
def handle_failed_app_launch(hash, try, tries, wait_time)
|
629
|
+
out = hash[:out]
|
630
|
+
RunLoop.log_debug("Failed to launch app on try #{try + 1} of #{tries}.")
|
631
|
+
out.split($-0).each do |line|
|
632
|
+
RunLoop.log_debug(" #{line}")
|
633
|
+
end
|
634
|
+
# If we timed out on the launch, the CoreSimulator processes are quit
|
635
|
+
# (see above). If at all possible, we want to avoid terminating
|
636
|
+
# CoreSimulatorService, because it takes a long time to launch.
|
637
|
+
sleep(wait_time) if wait_time > 0
|
638
|
+
|
639
|
+
out
|
640
|
+
end
|
641
|
+
|
642
|
+
# @!visibility private
|
643
|
+
def attempt_to_launch_app_with_simctl
|
644
|
+
begin
|
645
|
+
hash = launch_app_with_simctl
|
646
|
+
rescue RunLoop::Xcrun::TimeoutError => e
|
647
|
+
hash = {
|
648
|
+
:exit_status => 1,
|
649
|
+
:out => e.message
|
650
|
+
}
|
651
|
+
# Simulator is probably in a bad state. Terminates the
|
652
|
+
# CoreSimulatorService. Restarting this service is expensive!
|
653
|
+
RunLoop::CoreSimulator.terminate_core_simulator_processes
|
654
|
+
Kernel.sleep(0.5)
|
655
|
+
launch_simulator
|
656
|
+
end
|
657
|
+
hash
|
658
|
+
end
|
659
|
+
|
660
|
+
# Required for support of iOS 7 CoreSimulators. Can be removed when
|
661
|
+
# Xcode support is dropped.
|
662
|
+
def sdk_gte_8?
|
663
|
+
device.version >= RunLoop::Version.new('8.0')
|
664
|
+
end
|
665
|
+
|
666
|
+
# The data directory for the the device.
|
667
|
+
#
|
668
|
+
# ~/Library/Developer/CoreSimulator/Devices/<UDID>/data
|
669
|
+
def device_data_dir
|
670
|
+
@device_data_dir ||= File.join(CORE_SIMULATOR_DEVICE_DIR, device.udid, 'data')
|
671
|
+
end
|
672
|
+
|
673
|
+
# The applications directory for the device.
|
674
|
+
#
|
675
|
+
# ~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Bundle/Application
|
676
|
+
def device_applications_dir
|
677
|
+
@device_app_dir ||= lambda do
|
678
|
+
if sdk_gte_8?
|
679
|
+
File.join(device_data_dir, 'Containers', 'Bundle', 'Application')
|
680
|
+
else
|
681
|
+
File.join(device_data_dir, 'Applications')
|
682
|
+
end
|
683
|
+
end.call
|
684
|
+
end
|
685
|
+
|
686
|
+
# The sandbox directory for the app.
|
687
|
+
#
|
688
|
+
# ~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Data/Application
|
689
|
+
#
|
690
|
+
# Contains Library, Documents, and tmp directories.
|
691
|
+
def app_sandbox_dir
|
692
|
+
app_install_dir = installed_app_bundle_dir
|
693
|
+
return nil if app_install_dir.nil?
|
694
|
+
if sdk_gte_8?
|
695
|
+
app_sandbox_dir_sdk_gte_8
|
696
|
+
else
|
697
|
+
app_install_dir
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
def app_sandbox_dir_sdk_gte_8
|
702
|
+
containers_data_dir = File.join(device_data_dir, 'Containers', 'Data', 'Application')
|
703
|
+
apps = Dir.glob("#{containers_data_dir}/**/#{METADATA_PLIST}")
|
704
|
+
match = apps.find do |metadata_plist|
|
705
|
+
pbuddy.plist_read('MCMMetadataIdentifier', metadata_plist) == app.bundle_identifier
|
706
|
+
end
|
707
|
+
if match
|
708
|
+
File.dirname(match)
|
709
|
+
else
|
710
|
+
nil
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
# The Library directory in the sandbox.
|
715
|
+
def app_library_dir
|
716
|
+
base_dir = app_sandbox_dir
|
717
|
+
if base_dir.nil?
|
718
|
+
nil
|
719
|
+
else
|
720
|
+
File.join(base_dir, 'Library')
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
724
|
+
# The Library/Preferences directory in the sandbox.
|
725
|
+
def app_library_preferences_dir
|
726
|
+
base_dir = app_library_dir
|
727
|
+
if base_dir.nil?
|
728
|
+
nil
|
729
|
+
else
|
730
|
+
File.join(base_dir, 'Preferences')
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
# The Documents directory in the sandbox.
|
735
|
+
def app_documents_dir
|
736
|
+
base_dir = app_sandbox_dir
|
737
|
+
if base_dir.nil?
|
738
|
+
nil
|
739
|
+
else
|
740
|
+
File.join(base_dir, 'Documents')
|
741
|
+
end
|
742
|
+
end
|
743
|
+
|
744
|
+
# The tmp directory in the sandbox.
|
745
|
+
def app_tmp_dir
|
746
|
+
base_dir = app_sandbox_dir
|
747
|
+
if base_dir.nil?
|
748
|
+
nil
|
749
|
+
else
|
750
|
+
File.join(base_dir, 'tmp')
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
# A cache of installed apps on the device.
|
755
|
+
def device_caches_dir
|
756
|
+
@device_caches_dir ||= File.join(device_data_dir, 'Library', 'Caches')
|
757
|
+
end
|
758
|
+
|
759
|
+
# Required after when installing and uninstalling.
|
760
|
+
def clear_device_launch_csstore
|
761
|
+
glob = File.join(device_caches_dir, "com.apple.LaunchServices-*.csstore")
|
762
|
+
Dir.glob(glob) do | ccstore |
|
763
|
+
FileUtils.rm_f ccstore
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
# The sha1 of the installed app.
|
768
|
+
def installed_app_sha1
|
769
|
+
installed_bundle = installed_app_bundle_dir
|
770
|
+
if installed_bundle
|
771
|
+
RunLoop::Directory.directory_digest(installed_bundle)
|
772
|
+
else
|
773
|
+
nil
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
# Is the app that is install the same as the one we have in hand?
|
778
|
+
def same_sha1_as_installed?
|
779
|
+
app.sha1 == installed_app_sha1
|
780
|
+
end
|
781
|
+
|
782
|
+
# Returns the path to the installed app bundle directory (.app).
|
783
|
+
#
|
784
|
+
# If this method returns nil, the app is not installed.
|
785
|
+
def installed_app_bundle_dir
|
786
|
+
sim_app_dir = device_applications_dir
|
787
|
+
return nil if !File.exist?(sim_app_dir)
|
788
|
+
|
789
|
+
app_bundle_dir = Dir.glob("#{sim_app_dir}/**/*.app").find do |path|
|
790
|
+
RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
|
791
|
+
end
|
792
|
+
|
793
|
+
app_bundle_dir = ensure_complete_app_installation(app_bundle_dir)
|
794
|
+
|
795
|
+
app_bundle_dir
|
796
|
+
end
|
797
|
+
|
798
|
+
# Cleans up bad installations of an app. For unknown reasons, an app bundle
|
799
|
+
# can exist, but be unrecognized by CoreSimulator. If we detect a case like
|
800
|
+
# this, we need to clean up the installation.
|
801
|
+
def ensure_complete_app_installation(app_bundle_dir)
|
802
|
+
return nil if app_bundle_dir.nil?
|
803
|
+
return app_bundle_dir if complete_app_install?(app_bundle_dir)
|
804
|
+
|
805
|
+
# Remove the directory that contains the app bundle
|
806
|
+
base_dir = File.dirname(app_bundle_dir)
|
807
|
+
FileUtils.rm_rf(base_dir)
|
808
|
+
|
809
|
+
# Clean up Containers/Data/Application
|
810
|
+
remove_stale_data_containers
|
811
|
+
|
812
|
+
nil
|
813
|
+
end
|
814
|
+
|
815
|
+
# Detect an incomplete app installation.
|
816
|
+
def complete_app_install?(app_bundle_dir)
|
817
|
+
base_dir = File.dirname(app_bundle_dir)
|
818
|
+
plist = File.join(base_dir, METADATA_PLIST)
|
819
|
+
File.exist?(plist)
|
820
|
+
end
|
821
|
+
|
822
|
+
# Remove stale data directories that might have appeared as a result of an
|
823
|
+
# incomplete app installation.
|
824
|
+
# See #ensure_complete_app_installation
|
825
|
+
def remove_stale_data_containers
|
826
|
+
containers_data_dir = File.join(device_data_dir, "Containers", "Data", "Application")
|
827
|
+
apps = Dir.glob("#{containers_data_dir}/**/#{METADATA_PLIST}")
|
828
|
+
apps.each do |metadata_plist|
|
829
|
+
if pbuddy.plist_read("MCMMetadataIdentifier", metadata_plist) == app.bundle_identifier
|
830
|
+
FileUtils.rm_rf(File.dirname(metadata_plist))
|
831
|
+
end
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
# 1. Does nothing if the app is not installed.
|
836
|
+
# 2. Does nothing if the app the same as the app that is installed
|
837
|
+
# 3. Installs app if it is different from the installed app
|
838
|
+
def ensure_app_same
|
839
|
+
installed_app_bundle = installed_app_bundle_dir
|
840
|
+
|
841
|
+
if !installed_app_bundle
|
842
|
+
RunLoop.log_debug("App: #{app} is not installed")
|
843
|
+
return true
|
844
|
+
end
|
845
|
+
|
846
|
+
installed_sha = installed_app_sha1
|
847
|
+
app_sha = app.sha1
|
848
|
+
|
849
|
+
if installed_sha == app_sha
|
850
|
+
RunLoop.log_debug("Installed app is the same as #{app}")
|
851
|
+
return true
|
852
|
+
end
|
853
|
+
|
854
|
+
RunLoop.log_debug("The app you are testing is not the same as the app that is installed.")
|
855
|
+
RunLoop.log_debug(" Installed app SHA: #{installed_sha}")
|
856
|
+
RunLoop.log_debug(" App to launch SHA: #{app_sha}")
|
857
|
+
RunLoop.log_debug("Will install #{app}")
|
858
|
+
|
859
|
+
FileUtils.rm_rf installed_app_bundle
|
860
|
+
RunLoop.log_debug('Deleted the existing app')
|
861
|
+
|
862
|
+
directory = File.expand_path(File.join(installed_app_bundle, '..'))
|
863
|
+
bundle_name = File.basename(app.path)
|
864
|
+
target = File.join(directory, bundle_name)
|
865
|
+
|
866
|
+
args = ['ditto', app.path, target]
|
867
|
+
xcrun.run_command_in_context(args, log_cmd: true)
|
868
|
+
|
869
|
+
RunLoop.log_debug("Installed #{app} on CoreSimulator #{device.udid}")
|
870
|
+
|
871
|
+
clear_device_launch_csstore
|
872
|
+
|
873
|
+
true
|
874
|
+
end
|
875
|
+
|
876
|
+
# Shared tasks across CoreSimulators iOS 7 and > iOS 7
|
877
|
+
def reset_app_sandbox_internal_shared
|
878
|
+
[app_documents_dir, app_tmp_dir].each do |dir|
|
879
|
+
FileUtils.rm_rf dir
|
880
|
+
FileUtils.mkdir dir
|
881
|
+
end
|
882
|
+
end
|
883
|
+
|
884
|
+
# @!visibility private
|
885
|
+
def reset_app_sandbox_internal_sdk_gte_8
|
886
|
+
lib_dir = app_library_dir
|
887
|
+
RunLoop::Directory.recursive_glob_for_entries(lib_dir).each do |entry|
|
888
|
+
if entry.include?('Preferences')
|
889
|
+
# nop
|
890
|
+
else
|
891
|
+
if File.exist?(entry)
|
892
|
+
FileUtils.rm_rf(entry)
|
893
|
+
end
|
894
|
+
end
|
895
|
+
end
|
896
|
+
|
897
|
+
prefs_dir = app_library_preferences_dir
|
898
|
+
protected = ['com.apple.UIAutomation.plist',
|
899
|
+
'com.apple.UIAutomationPlugIn.plist']
|
900
|
+
RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
|
901
|
+
unless protected.include?(File.basename(entry))
|
902
|
+
if File.exist?(entry)
|
903
|
+
FileUtils.rm_rf entry
|
904
|
+
end
|
905
|
+
end
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
# @!visibility private
|
910
|
+
def reset_app_sandbox_internal_sdk_lt_8
|
911
|
+
prefs_dir = app_library_preferences_dir
|
912
|
+
RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
|
913
|
+
if entry.end_with?('.GlobalPreferences.plist') ||
|
914
|
+
entry.end_with?('com.apple.PeoplePicker.plist')
|
915
|
+
# nop
|
916
|
+
else
|
917
|
+
if File.exist?(entry)
|
918
|
+
FileUtils.rm_rf entry
|
919
|
+
end
|
920
|
+
end
|
921
|
+
end
|
922
|
+
|
923
|
+
# app preferences lives in device Library/Preferences
|
924
|
+
device_prefs_dir = File.join(app_sandbox_dir, 'Library', 'Preferences')
|
925
|
+
app_prefs_plist = File.join(device_prefs_dir, "#{app.bundle_identifier}.plist")
|
926
|
+
if File.exist?(app_prefs_plist)
|
927
|
+
FileUtils.rm_rf(app_prefs_plist)
|
928
|
+
end
|
929
|
+
end
|
930
|
+
|
931
|
+
# @!visibility private
|
932
|
+
def reset_app_sandbox_internal
|
933
|
+
reset_app_sandbox_internal_shared
|
934
|
+
|
935
|
+
if sdk_gte_8?
|
936
|
+
reset_app_sandbox_internal_sdk_gte_8
|
937
|
+
else
|
938
|
+
reset_app_sandbox_internal_sdk_lt_8
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
# Not yet. Failing on Travis and this is not a feature yet.
|
943
|
+
#
|
944
|
+
# There is a spec that has been commented out.
|
945
|
+
# @!visibility private
|
946
|
+
# TODO Command line tool
|
947
|
+
# def app_uia_crash_logs
|
948
|
+
# base_dir = app_library_dir
|
949
|
+
# if base_dir.nil?
|
950
|
+
# nil
|
951
|
+
# else
|
952
|
+
# dir = File.join(base_dir, 'CrashReporter', 'UIALogs')
|
953
|
+
# if Dir.exist?(dir)
|
954
|
+
# Dir.glob("#{dir}/*.plist")
|
955
|
+
# else
|
956
|
+
# nil
|
957
|
+
# end
|
958
|
+
# end
|
959
|
+
# end
|
960
|
+
end
|