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