run_loop_tcc 2.1.3

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