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