run_loop 0.2.1 → 1.0.0.pre3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/run_loop.rb +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
|