run_loop 0.2.1 → 1.0.0.pre3
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 +4 -1
- data/lib/run_loop/core.rb +94 -46
- data/lib/run_loop/device.rb +22 -0
- data/lib/run_loop/plist_buddy.rb +173 -0
- data/lib/run_loop/sim_control.rb +787 -0
- data/lib/run_loop/version.rb +161 -1
- data/lib/run_loop/xctools.rb +184 -0
- data/scripts/calabash.lldb.erb +4 -0
- metadata +179 -29
- data/.gitignore +0 -22
- data/.gitmodules +0 -3
- data/.irbrc +0 -19
- data/CHANGES.txt +0 -1
- data/Gemfile +0 -4
- data/README.md +0 -7
- data/Rakefile +0 -2
- data/build_to_run_loop.sh +0 -2
- data/docs/intro.md +0 -17
- data/irb.sh +0 -2
- data/run_loop.gemspec +0 -22
@@ -0,0 +1,787 @@
|
|
1
|
+
require 'cfpropertylist'
|
2
|
+
|
3
|
+
module RunLoop
|
4
|
+
|
5
|
+
# One class interact with the iOS Simulators.
|
6
|
+
#
|
7
|
+
# @note All command line tools are run in the context of `xcrun`.
|
8
|
+
#
|
9
|
+
# Throughout this class's documentation, there are references to the
|
10
|
+
# _current version of Xcode_. The current Xcode version is the one returned
|
11
|
+
# by `xcrun xcodebuild`. The current Xcode version can be set using
|
12
|
+
# `xcode-select` or overridden using the `DEVELOPER_DIR`.
|
13
|
+
#
|
14
|
+
# @todo `puts` calls need to be replaced with proper logging
|
15
|
+
class SimControl
|
16
|
+
|
17
|
+
# Returns an instance of XCTools.
|
18
|
+
# @return [RunLoop::XCTools] The xcode tools instance that is used internally.
|
19
|
+
def xctools
|
20
|
+
@xctools ||= RunLoop::XCTools.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# @!visibility private
|
24
|
+
# Are we running Xcode 6 or above?
|
25
|
+
#
|
26
|
+
# This is a convenience method.
|
27
|
+
#
|
28
|
+
# @return [Boolean] `true` if the current Xcode version is >= 6.0
|
29
|
+
def xcode_version_gte_6?
|
30
|
+
xctools.xcode_version_gte_6?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return an instance of PlistBuddy.
|
34
|
+
# @return [RunLoop::PlistBuddy] The plist buddy instance that is used internally.
|
35
|
+
def pbuddy
|
36
|
+
@pbuddy ||= RunLoop::PlistBuddy.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is the simulator for the current version of Xcode running?
|
40
|
+
# @return [Boolean] True if the simulator is running.
|
41
|
+
def sim_is_running?
|
42
|
+
not sim_pid.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
# If it is running, quit the simulator for the current version of Xcode.
|
46
|
+
#
|
47
|
+
# @param [Hash] opts Optional controls.
|
48
|
+
# @option opts [Float] :post_quit_wait (1.0) How long to sleep after the
|
49
|
+
# simulator has quit.
|
50
|
+
#
|
51
|
+
# @todo Consider migrating apple script call to xctools.
|
52
|
+
def quit_sim(opts={})
|
53
|
+
if sim_is_running?
|
54
|
+
default_opts = {:post_quit_wait => 1.0 }
|
55
|
+
merged_opts = default_opts.merge(opts)
|
56
|
+
`echo 'application "#{sim_name}" quit' | xcrun osascript`
|
57
|
+
sleep(merged_opts[:post_quit_wait]) if merged_opts[:post_quit_wait]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# If it is not already running, launch the simulator for the current version
|
62
|
+
# of Xcode.
|
63
|
+
#
|
64
|
+
# @param [Hash] opts Optional controls.
|
65
|
+
# @option opts [Float] :post_launch_wait (2.0) How long to sleep after the
|
66
|
+
# simulator has launched.
|
67
|
+
# @option opts [Boolean] :hide_after (false) If true, will attempt to Hide
|
68
|
+
# the simulator after it is launched. This is useful `only when testing
|
69
|
+
# gem features` that require the simulator be launched repeated and you are
|
70
|
+
# tired of your editor losing focus. :)
|
71
|
+
#
|
72
|
+
# @todo Consider migrating apple script call to xctools.
|
73
|
+
def launch_sim(opts={})
|
74
|
+
unless sim_is_running?
|
75
|
+
default_opts = {:post_launch_wait => 2.0,
|
76
|
+
:hide_after => false}
|
77
|
+
merged_opts = default_opts.merge(opts)
|
78
|
+
`xcrun open -a "#{sim_app_path}"`
|
79
|
+
if merged_opts[:hide_after]
|
80
|
+
`xcrun /usr/bin/osascript -e 'tell application "System Events" to keystroke "h" using command down'`
|
81
|
+
end
|
82
|
+
sleep(merged_opts[:post_launch_wait]) if merged_opts[:post_launch_wait]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Relaunch the simulator for the current version of Xcode. If that
|
87
|
+
# simulator is already running, it is quit.
|
88
|
+
#
|
89
|
+
# @param [Hash] opts Optional controls.
|
90
|
+
# @option opts [Float] :post_quit_wait (1.0) How long to sleep after the
|
91
|
+
# simulator has quit.
|
92
|
+
# @option opts [Float] :post_launch_wait (2.0) How long to sleep after the
|
93
|
+
# simulator has launched.
|
94
|
+
# @option opts [Boolean] :hide_after (false) If true, will attempt to Hide
|
95
|
+
# the simulator after it is launched. This is useful `only when testing
|
96
|
+
# gem features` that require the simulator be launched repeated and you are
|
97
|
+
# tired of your editor losing focus. :)
|
98
|
+
def relaunch_sim(opts={})
|
99
|
+
default_opts = {:post_quit_wait => 1.0,
|
100
|
+
:post_launch_wait => 2.0,
|
101
|
+
:hide_after => false}
|
102
|
+
merged_opts = default_opts.merge(opts)
|
103
|
+
quit_sim(merged_opts)
|
104
|
+
launch_sim(merged_opts)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Terminates all simulators.
|
108
|
+
#
|
109
|
+
# @note Sends `kill -9` to all Simulator processes. Use sparingly or not
|
110
|
+
# at all.
|
111
|
+
#
|
112
|
+
# SimulatorBridge
|
113
|
+
# launchd_sim
|
114
|
+
# ScriptAgent
|
115
|
+
#
|
116
|
+
# There can be only one simulator running at a time. However, during
|
117
|
+
# gem testing, situations can arise where multiple simulators are active.
|
118
|
+
def self.terminate_all_sims
|
119
|
+
|
120
|
+
# @todo Throwing SpringBoard crashed UI dialog.
|
121
|
+
# Tried the gentle approach first; it did not work.
|
122
|
+
# SimControl.new.quit_sim({:post_quit_wait => 0.5})
|
123
|
+
|
124
|
+
processes =
|
125
|
+
['iPhone Simulator.app', 'iOS Simulator.app',
|
126
|
+
|
127
|
+
# Multiple launchd_sim processes have been causing problems. This
|
128
|
+
# is a first pass at investigating what it would mean to kill the
|
129
|
+
# launchd_sim process.
|
130
|
+
'launchd_sim'
|
131
|
+
|
132
|
+
# RE: Throwing SpringBoard crashed UI dialog
|
133
|
+
# These are children of launchd_sim. I tried quiting them
|
134
|
+
# to suppress related UI dialogs about crashing processes. Killing
|
135
|
+
# them can throw 'launchd_sim' UI Dialogs
|
136
|
+
#'SimulatorBridge', 'SpringBoard', 'ScriptAgent', 'configd_sim', 'xpcproxy_sim'
|
137
|
+
]
|
138
|
+
|
139
|
+
# @todo Maybe should try to send -TERM first and -KILL if TERM fails.
|
140
|
+
# @todo Needs benchmarking.
|
141
|
+
processes.each do |process_name|
|
142
|
+
descripts = `xcrun ps x -o pid,command | grep "#{process_name}" | grep -v grep`.strip.split("\n")
|
143
|
+
descripts.each do |process_desc|
|
144
|
+
pid = process_desc.split(' ').first
|
145
|
+
Open3.popen3("xcrun kill -9 #{pid} && xcrun wait #{pid}") do |_, stdout, stderr, _|
|
146
|
+
if ENV['DEBUG_UNIX_CALLS'] == '1'
|
147
|
+
out = stdout.read.strip
|
148
|
+
err = stderr.read.strip
|
149
|
+
next if out.to_s.empty? and err.to_s.empty?
|
150
|
+
puts "kill process '#{pid}' => stdout: '#{out}' | stderr: '#{err}'"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Resets the simulator content and settings. It is analogous to touching
|
158
|
+
# the menu item _for every simulator_, regardless of SDK.
|
159
|
+
#
|
160
|
+
#
|
161
|
+
# On Xcode 5, it works by deleting the following directories:
|
162
|
+
#
|
163
|
+
# * ~/Library/Application Support/iPhone Simulator/Library
|
164
|
+
# * ~/Library/Application Support/iPhone Simulator/Library/<sdk>[-64]
|
165
|
+
#
|
166
|
+
# and relaunching the iOS Simulator which will recreate the Library
|
167
|
+
# directory and the latest SDK directory.
|
168
|
+
#
|
169
|
+
# On Xcode 6, it uses the `simctl erase <udid>` command line tool.
|
170
|
+
#
|
171
|
+
# @param [Hash] opts Optional controls for quitting and launching the simulator.
|
172
|
+
# @option opts [Float] :post_quit_wait (1.0) How long to sleep after the
|
173
|
+
# simulator has quit.
|
174
|
+
# @option opts [Float] :post_launch_wait (3.0) How long to sleep after the
|
175
|
+
# simulator has launched. Waits longer than normal because we need the
|
176
|
+
# simulator directories to be repopulated. **NOTE:** This option is ignored
|
177
|
+
# in Xcode 6.
|
178
|
+
# @option opts [Boolean] :hide_after (false) If true, will attempt to Hide
|
179
|
+
# the simulator after it is launched. This is useful `only when testing
|
180
|
+
# gem features` that require the simulator be launched repeated and you are
|
181
|
+
# tired of your editor losing focus. :) **NOTE:** This option is ignored
|
182
|
+
# in Xcode 6.
|
183
|
+
def reset_sim_content_and_settings(opts={})
|
184
|
+
default_opts = {:post_quit_wait => 1.0,
|
185
|
+
:post_launch_wait => 3.0,
|
186
|
+
:hide_after => false}
|
187
|
+
merged_opts = default_opts.merge(opts)
|
188
|
+
|
189
|
+
quit_sim(merged_opts)
|
190
|
+
|
191
|
+
# WARNING - DO NOT TRY TO DELETE Developer/CoreSimulator/Devices!
|
192
|
+
# Very bad things will happen. Unlike Xcode < 6, the re-launching the
|
193
|
+
# simulator will _not_ recreate the SDK (aka Devices) directories.
|
194
|
+
if xcode_version_gte_6?
|
195
|
+
simctl_reset
|
196
|
+
else
|
197
|
+
sim_lib_path = File.join(sim_app_support_dir, 'Library')
|
198
|
+
FileUtils.rm_rf(sim_lib_path)
|
199
|
+
existing_sim_sdk_or_device_data_dirs.each do |dir|
|
200
|
+
FileUtils.rm_rf(dir)
|
201
|
+
end
|
202
|
+
launch_sim(merged_opts)
|
203
|
+
|
204
|
+
# This is tricky because we need to wait for the simulator to recreate
|
205
|
+
# the directories. Specifically, we need the Accessibility plist to be
|
206
|
+
# exist so subsequent calabash launches will be able to enable
|
207
|
+
# accessibility.
|
208
|
+
#
|
209
|
+
# The directories take ~3.0 - ~5.0 to create.
|
210
|
+
counter = 0
|
211
|
+
loop do
|
212
|
+
break if counter == 80
|
213
|
+
dirs = existing_sim_sdk_or_device_data_dirs
|
214
|
+
if dirs.count == 0
|
215
|
+
sleep(0.2)
|
216
|
+
else
|
217
|
+
break if dirs.all? { |dir|
|
218
|
+
plist = File.expand_path("#{dir}/Library/Preferences/com.apple.Accessibility.plist")
|
219
|
+
File.exist?(plist)
|
220
|
+
}
|
221
|
+
sleep(0.2)
|
222
|
+
end
|
223
|
+
counter = counter + 1
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# @!visibility private
|
229
|
+
# Enables accessibility on all iOS Simulators by adjusting the
|
230
|
+
# simulator's Library/Preferences/com.apple.Accessibility.plist contents.
|
231
|
+
#
|
232
|
+
# A simulator 'exists' if has an Application Support directory. for
|
233
|
+
# example, the 6.1, 7.0.3-64, and 7.1 simulators exist if the following
|
234
|
+
# directories are present:
|
235
|
+
#
|
236
|
+
# ~/Library/Application Support/iPhone Simulator/Library/6.1
|
237
|
+
# ~/Library/Application Support/iPhone Simulator/Library/7.0.3-64
|
238
|
+
# ~/Library/Application Support/iPhone Simulator/Library/7.1
|
239
|
+
#
|
240
|
+
# A simulator is 'possible' if the SDK is available in the Xcode version.
|
241
|
+
#
|
242
|
+
# This method merges (uniquely) the possible and existing SDKs.
|
243
|
+
#
|
244
|
+
# This method also hides the AXInspector.
|
245
|
+
#
|
246
|
+
# **Q:** _Why do we need to enable for both existing and possible SDKs?_
|
247
|
+
# **A:** Consider what would happen if we were launching against the 7.0.3
|
248
|
+
# SDK for the first time. The 7.0.3 SDK directory does not exist _until the
|
249
|
+
# simulator has been launched_. The upshot is that we need to create the
|
250
|
+
# the plist _before_ we try to launch the simulator.
|
251
|
+
#
|
252
|
+
# @note This method will quit the current simulator.
|
253
|
+
#
|
254
|
+
# @param [Hash] opts controls the behavior of the method
|
255
|
+
# @option opts [Boolean] :verbose controls logging output
|
256
|
+
# @return [Boolean] true if enabling accessibility worked on all sdk
|
257
|
+
# directories
|
258
|
+
#
|
259
|
+
# @todo Should benchmark to see if memo-izing can help speed this up. Or if
|
260
|
+
# we can intuit the SDK and before launching and enable access on only
|
261
|
+
# that SDK.
|
262
|
+
#
|
263
|
+
# @todo Testing this is _hard_. ATM, I am using a reset sim content
|
264
|
+
# and settings + RunLoop.run to test.
|
265
|
+
def enable_accessibility_on_sims(opts={})
|
266
|
+
default_opts = {:verbose => false}
|
267
|
+
merged_opts = default_opts.merge(opts)
|
268
|
+
|
269
|
+
existing = existing_sim_sdk_or_device_data_dirs
|
270
|
+
|
271
|
+
if xcode_version_gte_6?
|
272
|
+
details = sim_details :udid
|
273
|
+
results = existing.map do |dir|
|
274
|
+
enable_accessibility_in_sim_data_dir(dir, details, opts)
|
275
|
+
end
|
276
|
+
else
|
277
|
+
possible = XCODE_5_SDKS.map do |sdk|
|
278
|
+
File.expand_path("~/Library/Application Support/iPhone Simulator/#{sdk}")
|
279
|
+
end
|
280
|
+
|
281
|
+
dirs = (possible + existing).uniq
|
282
|
+
results = dirs.map do |dir|
|
283
|
+
enable_accessibility_in_sdk_dir(dir, merged_opts)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
results.all?
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
|
292
|
+
# @!visibility private
|
293
|
+
# The list of possible SDKs for 5.0 <= Xcode < 6.0
|
294
|
+
#
|
295
|
+
# @note Used to enable automatically enable accessibility on the simulators.
|
296
|
+
#
|
297
|
+
# @see #enable_accessibility_on_sims
|
298
|
+
XCODE_5_SDKS = ['6.1', '7.0', '7.0.3', '7.0.3-64', '7.1', '7.1-64'].freeze
|
299
|
+
|
300
|
+
|
301
|
+
# @!visibility private
|
302
|
+
# A hash table of the accessibility properties that control whether or not
|
303
|
+
# accessibility is enabled and whether the AXInspector is visible.
|
304
|
+
#
|
305
|
+
# @note Xcode 5 or Xcode 6 SDK < 8.0
|
306
|
+
#
|
307
|
+
# @see #enable_accessibility_on_sims
|
308
|
+
SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH =
|
309
|
+
{
|
310
|
+
:access_enabled => {:key => 'AccessibilityEnabled',
|
311
|
+
:value => 'true',
|
312
|
+
:type => 'bool'},
|
313
|
+
|
314
|
+
:app_access_enabled => {:key => 'ApplicationAccessibilityEnabled',
|
315
|
+
:value => 'true',
|
316
|
+
:type => 'bool'},
|
317
|
+
|
318
|
+
:automation_enabled => {:key => 'AutomationEnabled',
|
319
|
+
:value => 'true',
|
320
|
+
:type => 'bool'},
|
321
|
+
|
322
|
+
# Determines if the Accessibility Inspector is showing.
|
323
|
+
#
|
324
|
+
# It turns out we can set this to 'false' as of Xcode 5.1 and
|
325
|
+
# hide the inspector altogether.
|
326
|
+
#
|
327
|
+
# I don't know what the behavior is on Xcode 5.0*.
|
328
|
+
:inspector_showing => {:key => 'AXInspectorEnabled',
|
329
|
+
:value => 'true',
|
330
|
+
:type => 'bool'},
|
331
|
+
|
332
|
+
# Controls if the Accessibility Inspector is expanded or not
|
333
|
+
# expanded.
|
334
|
+
:inspector_full_size => {:key => 'AXInspector.enabled',
|
335
|
+
:value => 'false',
|
336
|
+
:type => 'bool'},
|
337
|
+
|
338
|
+
# Controls the frame of the Accessibility Inspector.
|
339
|
+
# This is the best we can do because the OS will rewrite the
|
340
|
+
# frame if it does not conform to some expected range.
|
341
|
+
:inspector_frame => {:key => 'AXInspector.frame',
|
342
|
+
:value => '{{290, -13}, {276, 166}}',
|
343
|
+
:type => 'string'},
|
344
|
+
|
345
|
+
|
346
|
+
}.freeze
|
347
|
+
|
348
|
+
# @!visibility private
|
349
|
+
# A hash table of the accessibility properties that control whether or not
|
350
|
+
# accessibility is enabled and whether the AXInspector is visible.
|
351
|
+
#
|
352
|
+
# @note Xcode 6 SDK >= 8.0
|
353
|
+
#
|
354
|
+
# @see #enable_accessibility_in_sim_data_dir
|
355
|
+
SDK_80_ACCESSIBILITY_PROPERTIES_HASH =
|
356
|
+
{
|
357
|
+
:access_enabled => {:key => 'AccessibilityEnabled',
|
358
|
+
:value => 'true',
|
359
|
+
:type => 'bool'},
|
360
|
+
|
361
|
+
:app_access_enabled => {:key => 'ApplicationAccessibilityEnabled',
|
362
|
+
:value => 1,
|
363
|
+
:type => 'integer'},
|
364
|
+
|
365
|
+
:automation_enabled => {:key => 'AutomationEnabled',
|
366
|
+
:value => 1,
|
367
|
+
:type => 'integer'},
|
368
|
+
|
369
|
+
# Determines if the Accessibility Inspector is showing.
|
370
|
+
# Hurray! We can turn this off in Xcode 6.
|
371
|
+
:inspector_showing => {:key => 'AXInspectorEnabled',
|
372
|
+
:value => 0,
|
373
|
+
:type => 'integer'},
|
374
|
+
|
375
|
+
# controls if the Accessibility Inspector is expanded or not expanded
|
376
|
+
:inspector_full_size => {:key => 'AXInspector.enabled',
|
377
|
+
:value => 'false',
|
378
|
+
:type => 'bool'},
|
379
|
+
|
380
|
+
# Controls the frame of the Accessibility Inspector
|
381
|
+
#
|
382
|
+
# In Xcode 6, positioning this is difficult because the OS
|
383
|
+
# rewrites the value if the frame does not conform to an
|
384
|
+
# expected range. This is the best we can do.
|
385
|
+
#
|
386
|
+
# But see :inspector_showing! Woot!
|
387
|
+
:inspector_frame => {:key => 'AXInspector.frame',
|
388
|
+
:value => '{{270, 0}, {276, 166}}',
|
389
|
+
:type => 'string'},
|
390
|
+
|
391
|
+
# new and shiny - looks interesting!
|
392
|
+
:automation_disable_faux_collection_cells =>
|
393
|
+
{
|
394
|
+
:key => 'AutomationDisableFauxCollectionCells',
|
395
|
+
:value => 1,
|
396
|
+
:type => 'integer'
|
397
|
+
}
|
398
|
+
}.freeze
|
399
|
+
|
400
|
+
# @!visibility private
|
401
|
+
# A regex for finding directories under ~/Library/Developer/CoreSimulator/Devices
|
402
|
+
# and parsing the output of `simctl list sessions`.
|
403
|
+
XCODE_6_SIM_UDID_REGEX = /[A-F0-9]{8}-([A-F0-9]{4}-){3}[A-F0-9]{12}/.freeze
|
404
|
+
|
405
|
+
# @!visibility private
|
406
|
+
# Returns the current Simulator pid.
|
407
|
+
#
|
408
|
+
# @note Will only search for the current Xcode simulator.
|
409
|
+
#
|
410
|
+
# @return [String, nil] The pid as a String or nil if no process is found.
|
411
|
+
def sim_pid
|
412
|
+
`xcrun ps x -o pid,command | grep "#{sim_name}" | grep -v grep`.strip.split(' ').first
|
413
|
+
end
|
414
|
+
|
415
|
+
# @!visibility private
|
416
|
+
# Returns the current simulator name.
|
417
|
+
#
|
418
|
+
# @note In Xcode >= 6.0 the simulator name changed.
|
419
|
+
#
|
420
|
+
# @note Returns with the .app extension because on Xcode < 6.0, multiple
|
421
|
+
# processes can be found with 'iPhone Simulator'; the .app ensures that
|
422
|
+
# other methods find the right pid and application path.
|
423
|
+
# @return [String] A String suitable for searching for a pid, quitting, or
|
424
|
+
# launching the current simulator.
|
425
|
+
def sim_name
|
426
|
+
@sim_name ||= lambda {
|
427
|
+
if xcode_version_gte_6?
|
428
|
+
'iOS Simulator'
|
429
|
+
else
|
430
|
+
'iPhone Simulator'
|
431
|
+
end
|
432
|
+
}.call
|
433
|
+
end
|
434
|
+
|
435
|
+
# @!visibility private
|
436
|
+
# Returns the path to the current simulator.
|
437
|
+
#
|
438
|
+
# @note Xcode >= 6.0 the simulator app has a different path.
|
439
|
+
#
|
440
|
+
# @return [String] The path to the simulator app for the current version of
|
441
|
+
# Xcode.
|
442
|
+
def sim_app_path
|
443
|
+
@sim_app_path ||= lambda {
|
444
|
+
dev_dir = xctools.xcode_developer_dir
|
445
|
+
if xcode_version_gte_6?
|
446
|
+
"#{dev_dir}/Applications/iOS Simulator.app"
|
447
|
+
else
|
448
|
+
"#{dev_dir}/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app"
|
449
|
+
end
|
450
|
+
}.call
|
451
|
+
end
|
452
|
+
|
453
|
+
# @!visibility private
|
454
|
+
# The absolute path to the iPhone Simulator Application Support directory.
|
455
|
+
# @return [String] absolute path
|
456
|
+
def sim_app_support_dir
|
457
|
+
if xcode_version_gte_6?
|
458
|
+
File.expand_path('~/Library/Developer/CoreSimulator/Devices')
|
459
|
+
else
|
460
|
+
File.expand_path('~/Library/Application Support/iPhone Simulator')
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# @!visibility private
|
465
|
+
# In Xcode 5, this returns a list of absolute paths to the existing
|
466
|
+
# simulators SDK directories.
|
467
|
+
#
|
468
|
+
# In Xcode 6, this returns a list of absolute paths to the existing
|
469
|
+
# simulators `<udid>/data` directories.
|
470
|
+
#
|
471
|
+
# @note This can _never_ be memoized to a variable; its value reflects the
|
472
|
+
# state of the file system at the time it is called.
|
473
|
+
#
|
474
|
+
# In Xcode 5, a simulator 'exists' if it appears in the Application Support
|
475
|
+
# directory. For example, the 6.1, 7.0.3-64, and 7.1 simulators exist if
|
476
|
+
# the following directories are present:
|
477
|
+
#
|
478
|
+
# ```
|
479
|
+
# ~/Library/Application Support/iPhone Simulator/Library/6.1
|
480
|
+
# ~/Library/Application Support/iPhone Simulator/Library/7.0.3-64
|
481
|
+
# ~/Library/Application Support/iPhone Simulator/Library/7.1
|
482
|
+
# ```
|
483
|
+
#
|
484
|
+
# In Xcode 6, a simulator 'exists' if it appears in the
|
485
|
+
# CoreSimulator/Devices directory. For example:
|
486
|
+
#
|
487
|
+
# ```
|
488
|
+
# ~/Library/Developer/CoreSimulator/Devices/0BF52B67-F8BB-4246-A668-1880237DD17B
|
489
|
+
# ~/Library/Developer/CoreSimulator/Devices/2FCF6AFF-8C85-442F-B472-8D489ECBFAA5
|
490
|
+
# ~/Library/Developer/CoreSimulator/Devices/578A16BE-C31F-46E5-836E-66A2E77D89D4
|
491
|
+
# ```
|
492
|
+
#
|
493
|
+
# @example Xcode 5 behavior
|
494
|
+
# ~/Library/Application Support/iPhone Simulator/Library/6.1
|
495
|
+
# ~/Library/Application Support/iPhone Simulator/Library/7.0.3-64
|
496
|
+
# ~/Library/Application Support/iPhone Simulator/Library/7.1
|
497
|
+
#
|
498
|
+
# @example Xcode 6 behavior
|
499
|
+
# ~/Library/Developer/CoreSimulator/Devices/0BF52B67-F8BB-4246-A668-1880237DD17B/data
|
500
|
+
# ~/Library/Developer/CoreSimulator/Devices/2FCF6AFF-8C85-442F-B472-8D489ECBFAA5/data
|
501
|
+
# ~/Library/Developer/CoreSimulator/Devices/578A16BE-C31F-46E5-836E-66A2E77D89D4/data
|
502
|
+
#
|
503
|
+
# @return[Array<String>] a list of absolute paths to simulator directories
|
504
|
+
def existing_sim_sdk_or_device_data_dirs
|
505
|
+
base_dir = sim_app_support_dir
|
506
|
+
if xcode_version_gte_6?
|
507
|
+
regex = XCODE_6_SIM_UDID_REGEX
|
508
|
+
else
|
509
|
+
regex = /(\d)\.(\d)\.?(\d)?(-64)?/
|
510
|
+
end
|
511
|
+
dirs = Dir.glob("#{base_dir}/*").select { |path|
|
512
|
+
path =~ regex
|
513
|
+
}
|
514
|
+
|
515
|
+
if xcode_version_gte_6?
|
516
|
+
dirs.map { |elm| File.expand_path(File.join(elm, 'data')) }
|
517
|
+
else
|
518
|
+
dirs
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# @!visibility private
|
523
|
+
# Enables accessibility on the simulator indicated by `app_support_sdk_dir`.
|
524
|
+
#
|
525
|
+
# @note This will quit the simulator.
|
526
|
+
#
|
527
|
+
# @note This is for Xcode 5 only. Will raise an error if called on Xcode 6.
|
528
|
+
#
|
529
|
+
# @example
|
530
|
+
# path = '~/Library/Application Support/iPhone Simulator/6.1'
|
531
|
+
# enable_accessibility_in_sdk_dir(path)
|
532
|
+
#
|
533
|
+
# This method also hides the AXInspector.
|
534
|
+
#
|
535
|
+
# If the Library/Preferences/com.apple.Accessibility.plist does not exist
|
536
|
+
# this method will create a Library/Preferences/com.apple.Accessibility.plist
|
537
|
+
# that (oddly) the Simulator will _not_ overwrite.
|
538
|
+
#
|
539
|
+
# @see #enable_accessibility_on_sims for the public API.
|
540
|
+
#
|
541
|
+
# @param [String] app_support_sdk_dir the directory where the
|
542
|
+
# Library/Preferences/com.apple.Accessibility.plist can be found.
|
543
|
+
#
|
544
|
+
# @param [Hash] opts controls the behavior of the method
|
545
|
+
# @option opts [Boolean] :verbose controls logging output
|
546
|
+
# @return [Boolean] if the plist exists and the plist was successfully
|
547
|
+
# updated.
|
548
|
+
# @raise [RuntimeError] If called when Xcode 6 is the active Xcode version.
|
549
|
+
def enable_accessibility_in_sdk_dir(app_support_sdk_dir, opts={})
|
550
|
+
|
551
|
+
if xcode_version_gte_6?
|
552
|
+
raise RuntimeError, 'it is illegal to call this method when Xcode >= 6 is the current Xcode version'
|
553
|
+
end
|
554
|
+
|
555
|
+
default_opts = {:verbose => false}
|
556
|
+
merged_opts = default_opts.merge(opts)
|
557
|
+
|
558
|
+
quit_sim
|
559
|
+
|
560
|
+
verbose = merged_opts[:verbose]
|
561
|
+
sdk = File.basename(app_support_sdk_dir)
|
562
|
+
msgs = ["cannot enable accessibility for #{sdk} SDK"]
|
563
|
+
|
564
|
+
plist_path = File.expand_path("#{app_support_sdk_dir}/Library/Preferences/com.apple.Accessibility.plist")
|
565
|
+
|
566
|
+
if File.exist?(plist_path)
|
567
|
+
res = SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH.map do |hash_key, settings|
|
568
|
+
success = pbuddy.plist_set(settings[:key], settings[:type], settings[:value], plist_path)
|
569
|
+
unless success
|
570
|
+
if verbose
|
571
|
+
if settings[:type] == 'bool'
|
572
|
+
value = settings[:value] ? 'YES' : 'NO'
|
573
|
+
else
|
574
|
+
value = settings[:value]
|
575
|
+
end
|
576
|
+
msgs << "could not set #{hash_key} => '#{settings[:key]}' to #{value}"
|
577
|
+
puts "WARN: #{msgs.join("\n")}"
|
578
|
+
end
|
579
|
+
end
|
580
|
+
success
|
581
|
+
end
|
582
|
+
res.all?
|
583
|
+
else
|
584
|
+
FileUtils.mkdir_p("#{app_support_sdk_dir}/Library/Preferences")
|
585
|
+
plist = CFPropertyList::List.new
|
586
|
+
data = {}
|
587
|
+
plist.value = CFPropertyList.guess(data)
|
588
|
+
plist.save(plist_path, CFPropertyList::List::FORMAT_BINARY)
|
589
|
+
enable_accessibility_in_sdk_dir(app_support_sdk_dir, merged_opts)
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
# @!visibility private
|
594
|
+
# Enables accessibility on the simulator indicated by `sim_data_dir`.
|
595
|
+
#
|
596
|
+
# @note This will quit the simulator.
|
597
|
+
#
|
598
|
+
# @note This is for Xcode 6 only. Will raise an error if called on Xcode 5.
|
599
|
+
#
|
600
|
+
# @note The Accessibility plist contents differ by iOS version. For
|
601
|
+
# example, iOS 8 uses Number instead of Boolean as the data type for
|
602
|
+
# several entries. It is an _error_ to try to set a Number type to a
|
603
|
+
# Boolean value. This is why we need the second arg:
|
604
|
+
# `sim_details_key_with_udid` which is a hash that maps a sim udid to a
|
605
|
+
# a simulator version number. See the todo.
|
606
|
+
#
|
607
|
+
# @todo Should consider updating the API to pass just the version number instead
|
608
|
+
# of passing the entire sim_details hash.
|
609
|
+
#
|
610
|
+
# @example
|
611
|
+
# path = '~/Library/Developer/CoreSimulator/Devices/0BF52B67-F8BB-4246-A668-1880237DD17B'
|
612
|
+
# enable_accessibility_in_sim_data_dir(path, sim_details(:udid))
|
613
|
+
#
|
614
|
+
# This method also hides the AXInspector.
|
615
|
+
#
|
616
|
+
# If the Library/Preferences/com.apple.Accessibility.plist does not exist
|
617
|
+
# this method will create a Library/Preferences/com.apple.Accessibility.plist
|
618
|
+
# that (oddly) the Simulator will _not_ overwrite.
|
619
|
+
#
|
620
|
+
# @see #enable_accessibility_on_sims for the public API.
|
621
|
+
#
|
622
|
+
# @param [String] sim_data_dir The directory where the
|
623
|
+
# Library/Preferences/com.apple.Accessibility.plist can be found.
|
624
|
+
# @param [Hash] sim_details_key_with_udid A hash table of simulator details
|
625
|
+
# that can be obtained by calling `sim_details(:udid)`.
|
626
|
+
#
|
627
|
+
# @param [Hash] opts controls the behavior of the method
|
628
|
+
# @option opts [Boolean] :verbose controls logging output
|
629
|
+
# @return [Boolean] if the plist exists and the plist was successfully
|
630
|
+
# updated.
|
631
|
+
# @raise [RuntimeError] If called when Xcode 6 is _not_ the active Xcode version.
|
632
|
+
def enable_accessibility_in_sim_data_dir(sim_data_dir, sim_details_key_with_udid, opts={})
|
633
|
+
unless xcode_version_gte_6?
|
634
|
+
raise RuntimeError, 'it is illegal to call this method when the Xcode < 6 is the current Xcode version'
|
635
|
+
end
|
636
|
+
|
637
|
+
default_opts = {:verbose => false}
|
638
|
+
merged_opts = default_opts.merge(opts)
|
639
|
+
|
640
|
+
quit_sim
|
641
|
+
|
642
|
+
verbose = merged_opts[:verbose]
|
643
|
+
target_udid = sim_data_dir[XCODE_6_SIM_UDID_REGEX, 0]
|
644
|
+
launch_name = sim_details_key_with_udid[target_udid][:launch_name]
|
645
|
+
sdk_version = sim_details_key_with_udid[target_udid][:sdk_version]
|
646
|
+
|
647
|
+
msgs = ["cannot enable accessibility for '#{target_udid}' - '#{launch_name}'"]
|
648
|
+
plist_path = File.expand_path("#{sim_data_dir}/Library/Preferences/com.apple.Accessibility.plist")
|
649
|
+
|
650
|
+
if sdk_version >= RunLoop::Version.new('8.0')
|
651
|
+
hash = SDK_80_ACCESSIBILITY_PROPERTIES_HASH
|
652
|
+
else
|
653
|
+
hash = SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH
|
654
|
+
end
|
655
|
+
|
656
|
+
unless File.exist? plist_path
|
657
|
+
FileUtils.mkdir_p("#{sim_data_dir}/Library/Preferences")
|
658
|
+
plist = CFPropertyList::List.new
|
659
|
+
data = {}
|
660
|
+
plist.value = CFPropertyList.guess(data)
|
661
|
+
plist.save(plist_path, CFPropertyList::List::FORMAT_BINARY)
|
662
|
+
end
|
663
|
+
|
664
|
+
res = hash.map do |hash_key, settings|
|
665
|
+
success = pbuddy.plist_set(settings[:key], settings[:type], settings[:value], plist_path)
|
666
|
+
unless success
|
667
|
+
if verbose
|
668
|
+
if settings[:type] == 'bool'
|
669
|
+
value = settings[:value] ? 'YES' : 'NO'
|
670
|
+
else
|
671
|
+
value = settings[:value]
|
672
|
+
end
|
673
|
+
msgs << "could not set #{hash_key} => '#{settings[:key]}' to #{value}"
|
674
|
+
puts "WARN: #{msgs.join("\n")}"
|
675
|
+
end
|
676
|
+
end
|
677
|
+
success
|
678
|
+
end
|
679
|
+
res.all?
|
680
|
+
end
|
681
|
+
|
682
|
+
# @!visibility private
|
683
|
+
# Returns a hash table that contains detailed information about the
|
684
|
+
# available simulators. Use the `primary_key` to control the primary hash
|
685
|
+
# key. The same information is available regardless of the `primary_key`.
|
686
|
+
# Choose a key that matches your access pattern.
|
687
|
+
#
|
688
|
+
# @note This is for Xcode 6 only. Will raise an error if called on Xcode 5.
|
689
|
+
#
|
690
|
+
# @example :udid
|
691
|
+
# "FD50223C-C29E-497A-BF16-0D6451318251" => {
|
692
|
+
# :launch_name => "iPad Retina (7.1 Simulator)",
|
693
|
+
# :udid => "FD50223C-C29E-497A-BF16-0D6451318251",
|
694
|
+
# :sdk_version => #<RunLoop::Version:0x007f8ee8a9aac8 @major=7, @minor=1, @patch=nil>
|
695
|
+
# },
|
696
|
+
# "21DED687-77F5-4125-A480-0DBA6A1BA6D1" => {
|
697
|
+
# :launch_name => "iPad Retina (8.0 Simulator)",
|
698
|
+
# :udid => "21DED687-77F5-4125-A480-0DBA6A1BA6D1",
|
699
|
+
# :sdk_version => #<RunLoop::Version:0x007f8ee8a9a730 @major=8, @minor=0, @patch=nil>
|
700
|
+
# },
|
701
|
+
#
|
702
|
+
#
|
703
|
+
# @example :launch_name
|
704
|
+
# "iPad Retina (7.1 Simulator)" => {
|
705
|
+
# :launch_name => "iPad Retina (7.1 Simulator)",
|
706
|
+
# :udid => "FD50223C-C29E-497A-BF16-0D6451318251",
|
707
|
+
# :sdk_version => #<RunLoop::Version:0x007f8ee8a9aac8 @major=7, @minor=1, @patch=nil>
|
708
|
+
# },
|
709
|
+
# "iPad Retina (8.0 Simulator)" => {
|
710
|
+
# :launch_name => "iPad Retina (8.0 Simulator)",
|
711
|
+
# :udid => "21DED687-77F5-4125-A480-0DBA6A1BA6D1",
|
712
|
+
# :sdk_version => #<RunLoop::Version:0x007f8ee8a9a730 @major=8, @minor=0, @patch=nil>
|
713
|
+
# },
|
714
|
+
#
|
715
|
+
# @param [Symbol] primary_key Can be on of `{:udid | :launch_name}`.
|
716
|
+
# @raise [RuntimeError] If called when Xcode 6 is _not_ the active Xcode version.
|
717
|
+
# @raise [RuntimeError] If called with an invalid `primary_key`.
|
718
|
+
def sim_details(primary_key)
|
719
|
+
unless xcode_version_gte_6?
|
720
|
+
raise RuntimeError, 'this method is only available on Xcode >= 6'
|
721
|
+
end
|
722
|
+
|
723
|
+
allowed = [:udid, :launch_name]
|
724
|
+
unless allowed.include? primary_key
|
725
|
+
raise ArgumentError, "expected '#{primary_key}' to be one of '#{allowed}'"
|
726
|
+
end
|
727
|
+
|
728
|
+
hash = {}
|
729
|
+
xctools.instruments(:sims).each do |elm|
|
730
|
+
launch_name = elm[/\A.+\((\d\.\d(\.\d)? Simulator\))/, 0]
|
731
|
+
udid = elm[XCODE_6_SIM_UDID_REGEX,0]
|
732
|
+
sdk_version = elm[/(\d\.\d(\.\d)? Simulator)/, 0].split(' ').first
|
733
|
+
value =
|
734
|
+
{
|
735
|
+
:launch_name => launch_name,
|
736
|
+
:udid => udid,
|
737
|
+
:sdk_version => RunLoop::Version.new(sdk_version)
|
738
|
+
}
|
739
|
+
if primary_key == :udid
|
740
|
+
key = udid
|
741
|
+
else
|
742
|
+
key = launch_name
|
743
|
+
end
|
744
|
+
hash[key] = value
|
745
|
+
end
|
746
|
+
hash
|
747
|
+
end
|
748
|
+
|
749
|
+
# @!visibility private
|
750
|
+
# Uses the `simctl erase` command to reset the content and settings on _all_
|
751
|
+
# simulators.
|
752
|
+
#
|
753
|
+
# @todo Should this reset _every_ simulator or just the targeted simulator?
|
754
|
+
# It is very slow to reset every simulator and if we are trying to respond
|
755
|
+
# to RESET_BETWEEN_SCENARIOS we probably want to erase just the current
|
756
|
+
# simulator.
|
757
|
+
#
|
758
|
+
# @note This is an Xcode 6 only method. Will raise an error if called on
|
759
|
+
# Xcode < 6.
|
760
|
+
#
|
761
|
+
# @note This method will quit the simulator.
|
762
|
+
#
|
763
|
+
# @raise [RuntimeError] If called on Xcode < 6.
|
764
|
+
def simctl_reset
|
765
|
+
unless xcode_version_gte_6?
|
766
|
+
raise RuntimeError, 'this method is only available on Xcode >= 6'
|
767
|
+
end
|
768
|
+
|
769
|
+
quit_sim
|
770
|
+
|
771
|
+
sim_details = sim_details(:udid)
|
772
|
+
res = []
|
773
|
+
sim_details.each_key do |key|
|
774
|
+
cmd = "xcrun simctl erase #{key}"
|
775
|
+
Open3.popen3(cmd) do |_, stdout, stderr, _|
|
776
|
+
out = stdout.read.strip
|
777
|
+
err = stderr.read.strip
|
778
|
+
if ENV['DEBUG_UNIX_CALLS'] == '1'
|
779
|
+
puts "#{cmd} => stdout: '#{out}' | stderr: '#{err}'"
|
780
|
+
end
|
781
|
+
res << err.empty?
|
782
|
+
end
|
783
|
+
end
|
784
|
+
res.all?
|
785
|
+
end
|
786
|
+
end
|
787
|
+
end
|