run_loop 1.5.5 → 1.5.6.pre1

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,648 @@
1
+ # A class to manage interactions with CoreSimulators.
2
+ class RunLoop::CoreSimulator
3
+
4
+ # @!visibility private
5
+ attr_reader :app
6
+
7
+ # @!visibility private
8
+ attr_reader :device
9
+
10
+ # @!visibility private
11
+ attr_reader :pbuddy
12
+
13
+ # @!visibility private
14
+ attr_reader :xcode
15
+
16
+ # @!visibility private
17
+ attr_reader :xcrun
18
+
19
+ # @!visibility private
20
+ attr_reader :simulator_pid
21
+
22
+ # @!visibility private
23
+ METADATA_PLIST = '.com.apple.mobile_container_manager.metadata.plist'
24
+
25
+ # @!visibility private
26
+ CORE_SIMULATOR_DEVICE_DIR = File.expand_path('~/Library/Developer/CoreSimulator/Devices')
27
+
28
+ # @!visibility private
29
+ WAIT_FOR_DEVICE_STATE_OPTS = {
30
+ interval: 0.1,
31
+ timeout: 5
32
+ }
33
+
34
+ # @!visibility private
35
+ MANAGED_PROCESSES =
36
+ [
37
+ # This process is a daemon, and requires 'KILL' to terminate.
38
+ # Killing the process is fast, but it takes a long time to
39
+ # restart.
40
+ # ['com.apple.CoreSimulator.CoreSimulatorService', false],
41
+
42
+ # Probably do not need to quit this, but it is tempting to do so.
43
+ #['com.apple.CoreSimulator.SimVerificationService', false],
44
+
45
+ 'SimulatorBridge',
46
+ 'configd_sim',
47
+
48
+ # Does not always appear.
49
+ 'CoreSimulatorBridge',
50
+
51
+ # Xcode 7
52
+ 'ids_simd'
53
+ ]
54
+
55
+ # @!visibility private
56
+ # Pattern:
57
+ # [ '< process name >', < send term first > ]
58
+ SIMULATOR_QUIT_PROCESSES =
59
+ [
60
+ # Xcode 7 start throwing this error.
61
+ ['splashboardd', false],
62
+
63
+ # Xcode < 5.1
64
+ ['iPhone Simulator.app', true],
65
+
66
+ # 7.0 < Xcode <= 6.0
67
+ ['iOS Simulator.app', true],
68
+
69
+ # Xcode >= 7.0
70
+ ['Simulator.app', true],
71
+
72
+ # Multiple launchd_sim processes have been causing problems. This
73
+ # is a first pass at investigating what it would mean to kill the
74
+ # launchd_sim process.
75
+ ['launchd_sim', false],
76
+
77
+ # assetsd instances clobber each other and are not properly
78
+ # killed when quiting the simulator.
79
+ ['assetsd', false],
80
+
81
+ # iproxy is started by UITest.
82
+ ['iproxy', false],
83
+
84
+ # Started by Xamarin Studio, this is the parent process of the
85
+ # processes launched by Xamarin's interaction with
86
+ # CoreSimulatorBridge.
87
+ ['csproxy', false],
88
+ ]
89
+
90
+ # @!visibility private
91
+ #
92
+ # Terminate CoreSimulator related processes. This processes can accumulate
93
+ # as testing proceeds and can cause instability.
94
+ def self.terminate_core_simulator_processes
95
+
96
+ self.quit_simulator
97
+
98
+ MANAGED_PROCESSES.each do |process_name|
99
+ send_term_first = false
100
+ self.term_or_kill(process_name, send_term_first)
101
+ end
102
+ end
103
+
104
+ # @!visibility private
105
+ # Quit any Simulator.app or iOS Simulator.app
106
+ def self.quit_simulator
107
+ SIMULATOR_QUIT_PROCESSES.each do |process_details|
108
+ process_name = process_details[0]
109
+ send_term_first = process_details[1]
110
+ self.term_or_kill(process_name, send_term_first)
111
+ end
112
+ end
113
+
114
+ # @param [RunLoop::Device] device The device.
115
+ # @param [RunLoop::App] app The application.
116
+ # @param [Hash] options Controls the behavior of this class.
117
+ # @option options :quit_sim_on_init (true) If true, quit any running
118
+ # @option options :xcode An instance of Xcode to use
119
+ # simulators in the initialize method.
120
+ def initialize(device, app, options={})
121
+ defaults = { :quit_sim_on_init => true }
122
+ merged = defaults.merge(options)
123
+
124
+ @app = app
125
+ @device = device
126
+
127
+ @xcode = merged[:xcode]
128
+
129
+ if merged[:quit_sim_on_init]
130
+ RunLoop::CoreSimulator.quit_simulator
131
+ end
132
+
133
+ # stdio.pipe - can cause problems finding the SHA or size data dir
134
+ rm_instruments_pipe
135
+ end
136
+
137
+ # @!visibility private
138
+ def pbuddy
139
+ @pbuddy ||= RunLoop::PlistBuddy.new
140
+ end
141
+
142
+ # @!visibility private
143
+ def xcode
144
+ @xcode ||= RunLoop::Xcode.new
145
+ end
146
+
147
+ # @!visibility private
148
+ def xcrun
149
+ @xcrun ||= RunLoop::Xcrun.new
150
+ end
151
+
152
+ # @!visibility private
153
+ def simulator_pid
154
+ @simulator_pid
155
+ end
156
+
157
+ # Launch the simulator indicated by device.
158
+ def launch_simulator
159
+
160
+ if sim_pid != nil
161
+ # There is a running simulator.
162
+
163
+ # Did we launch it?
164
+ if sim_pid == simulator_pid
165
+ # Nothing to do, we already launched the simulator.
166
+ return
167
+ else
168
+ # We did not launch this simulator; quit it.
169
+ RunLoop::CoreSimulator.quit_simulator
170
+ end
171
+ end
172
+
173
+ args = ['open', '-g', '-a', sim_app_path, '--args', '-CurrentDeviceUDID', device.udid]
174
+
175
+ RunLoop.log_debug("Launching #{device} with:")
176
+ RunLoop.log_unix_cmd("xcrun #{args.join(' ')}")
177
+
178
+ start_time = Time.now
179
+
180
+ pid = spawn('xcrun', *args)
181
+ Process.detach(pid)
182
+
183
+ # Keep track of the pid so we can know if we have already launched this sim.
184
+ @simulator_pid = pid
185
+
186
+ options = { :timeout => 5, :raise_on_timeout => true }
187
+ RunLoop::ProcessWaiter.new(sim_name, options).wait_for_any
188
+
189
+ device.simulator_wait_for_stable_state
190
+
191
+ elapsed = Time.now - start_time
192
+ RunLoop.log_debug("Took #{elapsed} seconds to launch the simulator")
193
+
194
+ true
195
+ end
196
+
197
+ # Launch the app on the simulator.
198
+ #
199
+ # 1. If the app is not installed, it is installed.
200
+ # 2. If the app is different from the app that is installed, it is installed.
201
+ def launch
202
+ install
203
+
204
+ args = ['simctl', 'launch', device.udid, app.bundle_identifier]
205
+ hash = xcrun.exec(args, log_cmd: true, timeout: 20)
206
+
207
+ exit_status = hash[:exit_status]
208
+
209
+ if exit_status != 0
210
+ err = hash[:err]
211
+ RunLoop.log_error(err)
212
+ raise RuntimeError, "Could not launch #{app.bundle_identifier} on #{device}"
213
+ end
214
+
215
+ options = {
216
+ :timeout => 10,
217
+ :raise_on_timeout => true
218
+ }
219
+
220
+ RunLoop::ProcessWaiter.new(app.executable_name, options).wait_for_any
221
+
222
+ device.simulator_wait_for_stable_state
223
+ true
224
+ end
225
+
226
+ # Install the app.
227
+ #
228
+ # 1. If the app is not installed, it is installed.
229
+ # 2. Does nothing, if the app is the same as the one that is installed.
230
+ # 3. Installs the app if it is different from the installed app.
231
+ #
232
+ # The app sandbox is not touched.
233
+ def install
234
+ installed_app_bundle = installed_app_bundle_dir
235
+
236
+ # App is not installed. Use simctl interface to install.
237
+ if !installed_app_bundle
238
+ installed_app_bundle = install_app_with_simctl
239
+ else
240
+ ensure_app_same
241
+ end
242
+
243
+ installed_app_bundle
244
+ end
245
+
246
+ # Is this app installed?
247
+ def app_is_installed?
248
+ !installed_app_bundle_dir.nil?
249
+ end
250
+
251
+ # Resets the app sandbox.
252
+ #
253
+ # Does nothing if the app is not installed.
254
+ def reset_app_sandbox
255
+ return true if !app_is_installed?
256
+
257
+ wait_for_device_state('Shutdown')
258
+
259
+ reset_app_sandbox_internal
260
+ end
261
+
262
+ # Uninstalls the app and clears the sandbox.
263
+ def uninstall_app_and_sandbox
264
+ return true if !app_is_installed?
265
+
266
+ launch_simulator
267
+
268
+ args = ['simctl', 'uninstall', device.udid, app.bundle_identifier]
269
+ xcrun.exec(args, log_cmd: true, timeout: 20)
270
+
271
+ device.simulator_wait_for_stable_state
272
+ true
273
+ end
274
+
275
+ private
276
+
277
+ # @!visibility private
278
+ #
279
+ # This stdio.pipe file causes problems when checking the size and taking the
280
+ # checksum of the core simulator directory.
281
+ def rm_instruments_pipe
282
+ device_tmp_dir = File.join(device_data_dir, 'tmp')
283
+ Dir.glob("#{device_tmp_dir}/instruments_*/stdio.pipe") do |file|
284
+ if File.exist?(file)
285
+ RunLoop.log_debug("Deleting #{file}")
286
+ FileUtils.rm_rf(file)
287
+ end
288
+ end
289
+ end
290
+
291
+ # Send 'TERM' then 'KILL' to allow processes to quit cleanly.
292
+ def self.term_or_kill(process_name, send_term_first)
293
+ term_options = { :timeout => 0.5 }
294
+ kill_options = { :timeout => 0.5 }
295
+
296
+ RunLoop::ProcessWaiter.new(process_name).pids.each do |pid|
297
+ killed = false
298
+
299
+ if send_term_first
300
+ term = RunLoop::ProcessTerminator.new(pid, 'TERM', process_name, term_options)
301
+ killed = term.kill_process
302
+ end
303
+
304
+ unless killed
305
+ RunLoop::ProcessTerminator.new(pid, 'KILL', process_name, kill_options)
306
+ end
307
+ end
308
+ end
309
+ # Returns the current simulator name.
310
+ #
311
+ # @return [String] A String suitable for searching for a pid, quitting, or
312
+ # launching the current simulator.
313
+ def sim_name
314
+ @sim_name ||= lambda {
315
+ if xcode.version_gte_7?
316
+ 'Simulator'
317
+ elsif xcode.version_gte_6?
318
+ 'iOS Simulator'
319
+ else
320
+ 'iPhone Simulator'
321
+ end
322
+ }.call
323
+ end
324
+
325
+ # @!visibility private
326
+ # Returns the path to the current simulator.
327
+ #
328
+ # @return [String] The path to the simulator app for the current version of
329
+ # Xcode.
330
+ def sim_app_path
331
+ @sim_app_path ||= lambda {
332
+ dev_dir = xcode.developer_dir
333
+ if xcode.version_gte_7?
334
+ "#{dev_dir}/Applications/Simulator.app"
335
+ elsif xcode.version_gte_6?
336
+ "#{dev_dir}/Applications/iOS Simulator.app"
337
+ else
338
+ "#{dev_dir}/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app"
339
+ end
340
+ }.call
341
+ end
342
+
343
+ # @!visibility private
344
+ # Returns the current Simulator pid.
345
+ #
346
+ # @note Will only search for the current Xcode simulator.
347
+ #
348
+ # @return [String, nil] The pid as a String or nil if no process is found.
349
+ #
350
+ # @todo Convert this to force UTF8
351
+ def sim_pid
352
+ process_name = "MacOS/#{sim_name}"
353
+ `xcrun ps x -o pid,command | grep "#{process_name}" | grep -v grep`.strip.split(' ').first
354
+ end
355
+
356
+ # @!visibility private
357
+ def install_app_with_simctl
358
+ launch_simulator
359
+
360
+ args = ['simctl', 'install', device.udid, app.path]
361
+ xcrun.exec(args, log_cmd: true, timeout: 20)
362
+
363
+ device.simulator_wait_for_stable_state
364
+ installed_app_bundle_dir
365
+ end
366
+
367
+ # @!visibility private
368
+ def wait_for_device_state(target_state)
369
+ now = Time.now
370
+ timeout = WAIT_FOR_DEVICE_STATE_OPTS[:timeout]
371
+ poll_until = now + timeout
372
+ delay = WAIT_FOR_DEVICE_STATE_OPTS[:interval]
373
+ in_state = false
374
+ while Time.now < poll_until
375
+ in_state = device.update_simulator_state == target_state
376
+ break if in_state
377
+ sleep delay
378
+ end
379
+
380
+ elapsed = Time.now - now
381
+ RunLoop.log_debug("Waited for #{elapsed} seconds for device to have state: '#{target_state}'.")
382
+
383
+ unless in_state
384
+ raise "Expected '#{target_state} but found '#{device.state}' after waiting."
385
+ end
386
+ in_state
387
+ end
388
+
389
+ # Required for support of iOS 7 CoreSimulators. Can be removed when
390
+ # Xcode support is dropped.
391
+ def sdk_gte_8?
392
+ device.version >= RunLoop::Version.new('8.0')
393
+ end
394
+
395
+ # The data directory for the the device.
396
+ #
397
+ # ~/Library/Developer/CoreSimulator/Devices/<UDID>/data
398
+ def device_data_dir
399
+ @device_data_dir ||= File.join(CORE_SIMULATOR_DEVICE_DIR, device.udid, 'data')
400
+ end
401
+
402
+ # The applications directory for the device.
403
+ #
404
+ # ~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Bundle/Application
405
+ def device_applications_dir
406
+ @device_app_dir ||= lambda do
407
+ if sdk_gte_8?
408
+ File.join(device_data_dir, 'Containers', 'Bundle', 'Application')
409
+ else
410
+ File.join(device_data_dir, 'Applications')
411
+ end
412
+ end.call
413
+ end
414
+
415
+ # The sandbox directory for the app.
416
+ #
417
+ # ~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Data/Application
418
+ #
419
+ # Contains Library, Documents, and tmp directories.
420
+ def app_sandbox_dir
421
+ app_install_dir = installed_app_bundle_dir
422
+ return nil if app_install_dir.nil?
423
+ if sdk_gte_8?
424
+ app_sandbox_dir_sdk_gte_8
425
+ else
426
+ app_install_dir
427
+ end
428
+ end
429
+
430
+ def app_sandbox_dir_sdk_gte_8
431
+ containers_data_dir = File.join(device_data_dir, 'Containers', 'Data', 'Application')
432
+ apps = Dir.glob("#{containers_data_dir}/**/#{METADATA_PLIST}")
433
+ match = apps.find do |metadata_plist|
434
+ pbuddy.plist_read('MCMMetadataIdentifier', metadata_plist) == app.bundle_identifier
435
+ end
436
+ if match
437
+ File.dirname(match)
438
+ else
439
+ nil
440
+ end
441
+ end
442
+
443
+ # The Library directory in the sandbox.
444
+ def app_library_dir
445
+ base_dir = app_sandbox_dir
446
+ if base_dir.nil?
447
+ nil
448
+ else
449
+ File.join(base_dir, 'Library')
450
+ end
451
+ end
452
+
453
+ # The Library/Preferences directory in the sandbox.
454
+ def app_library_preferences_dir
455
+ base_dir = app_library_dir
456
+ if base_dir.nil?
457
+ nil
458
+ else
459
+ File.join(base_dir, 'Preferences')
460
+ end
461
+ end
462
+
463
+ # The Documents directory in the sandbox.
464
+ def app_documents_dir
465
+ base_dir = app_sandbox_dir
466
+ if base_dir.nil?
467
+ nil
468
+ else
469
+ File.join(base_dir, 'Documents')
470
+ end
471
+ end
472
+
473
+ # The tmp directory in the sandbox.
474
+ def app_tmp_dir
475
+ base_dir = app_sandbox_dir
476
+ if base_dir.nil?
477
+ nil
478
+ else
479
+ File.join(base_dir, 'tmp')
480
+ end
481
+ end
482
+
483
+ # A cache of installed apps on the device.
484
+ def device_caches_dir
485
+ @device_caches_dir ||= File.join(device_data_dir, 'Library', 'Caches')
486
+ end
487
+
488
+ # Required after when installing and uninstalling.
489
+ def clear_device_launch_csstore
490
+ glob = File.join(device_caches_dir, "com.apple.LaunchServices-*.csstore")
491
+ Dir.glob(glob) do | ccstore |
492
+ FileUtils.rm_f ccstore
493
+ end
494
+ end
495
+
496
+ # The sha1 of the installed app.
497
+ def installed_app_sha1
498
+ installed_bundle = installed_app_bundle_dir
499
+ if installed_bundle
500
+ RunLoop::Directory.directory_digest(installed_bundle)
501
+ else
502
+ nil
503
+ end
504
+ end
505
+
506
+ # Is the app that is install the same as the one we have in hand?
507
+ def same_sha1_as_installed?
508
+ app.sha1 == installed_app_sha1
509
+ end
510
+
511
+ # Returns the path to the installed app bundle directory (.app).
512
+ #
513
+ # If this method returns nil, the app is not installed.
514
+ def installed_app_bundle_dir
515
+ sim_app_dir = device_applications_dir
516
+ return nil if !File.exist?(sim_app_dir)
517
+ Dir.glob("#{sim_app_dir}/**/*.app").find do |path|
518
+ RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
519
+ end
520
+ end
521
+
522
+ # 1. Does nothing if the app is not installed.
523
+ # 2. Does nothing if the app the same as the app that is installed
524
+ # 3. Installs app if it is different from the installed app
525
+ #
526
+ def ensure_app_same
527
+ installed_app_bundle = installed_app_bundle_dir
528
+
529
+ if !installed_app_bundle
530
+ RunLoop.log_debug("App: #{app} is not installed")
531
+ return true
532
+ end
533
+
534
+ installed_sha = installed_app_sha1
535
+ app_sha = app.sha1
536
+
537
+ if installed_sha == app_sha
538
+ RunLoop.log_debug("Installed app is the same as #{app}")
539
+ return true
540
+ end
541
+
542
+ RunLoop.log_debug("The app you are are testing is not the same as the app that is installed.")
543
+ RunLoop.log_debug(" Installed app SHA: #{installed_sha}")
544
+ RunLoop.log_debug(" App to launch SHA: #{app_sha}")
545
+ RunLoop.log_debug("Will install #{app}")
546
+
547
+ FileUtils.rm_rf installed_app_bundle
548
+ RunLoop.log_debug('Deleted the existing app')
549
+
550
+ directory = File.expand_path(File.join(installed_app_bundle, '..'))
551
+ bundle_name = File.basename(app.path)
552
+ target = File.join(directory, bundle_name)
553
+
554
+ args = ['ditto', app.path, target]
555
+ xcrun.exec(args, log_cmd: true)
556
+
557
+ RunLoop.log_debug("Installed #{app} on CoreSimulator #{device.udid}")
558
+
559
+ clear_device_launch_csstore
560
+
561
+ true
562
+ end
563
+
564
+ # Shared tasks across CoreSimulators iOS 7 and > iOS 7
565
+ def reset_app_sandbox_internal_shared
566
+ [app_documents_dir, app_tmp_dir].each do |dir|
567
+ FileUtils.rm_rf dir
568
+ FileUtils.mkdir dir
569
+ end
570
+ end
571
+
572
+ # @!visibility private
573
+ def reset_app_sandbox_internal_sdk_gte_8
574
+ lib_dir = app_library_dir
575
+ RunLoop::Directory.recursive_glob_for_entries(lib_dir).each do |entry|
576
+ if entry.include?('Preferences')
577
+ # nop
578
+ else
579
+ if File.exist?(entry)
580
+ FileUtils.rm_rf(entry)
581
+ end
582
+ end
583
+ end
584
+
585
+ prefs_dir = app_library_preferences_dir
586
+ protected = ['com.apple.UIAutomation.plist',
587
+ 'com.apple.UIAutomationPlugIn.plist']
588
+ RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
589
+ unless protected.include?(File.basename(entry))
590
+ if File.exist?(entry)
591
+ FileUtils.rm_rf entry
592
+ end
593
+ end
594
+ end
595
+ end
596
+
597
+ # @!visibility private
598
+ def reset_app_sandbox_internal_sdk_lt_8
599
+ prefs_dir = app_library_preferences_dir
600
+ RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
601
+ if entry.end_with?('.GlobalPreferences.plist') ||
602
+ entry.end_with?('com.apple.PeoplePicker.plist')
603
+ # nop
604
+ else
605
+ if File.exist?(entry)
606
+ FileUtils.rm_rf entry
607
+ end
608
+ end
609
+ end
610
+
611
+ # app preferences lives in device Library/Preferences
612
+ device_prefs_dir = File.join(app_sandbox_dir, 'Library', 'Preferences')
613
+ app_prefs_plist = File.join(device_prefs_dir, "#{app.bundle_identifier}.plist")
614
+ if File.exist?(app_prefs_plist)
615
+ FileUtils.rm_rf(app_prefs_plist)
616
+ end
617
+ end
618
+
619
+ # @!visibility private
620
+ def reset_app_sandbox_internal
621
+ reset_app_sandbox_internal_shared
622
+
623
+ if sdk_gte_8?
624
+ reset_app_sandbox_internal_sdk_gte_8
625
+ else
626
+ reset_app_sandbox_internal_sdk_lt_8
627
+ end
628
+ end
629
+
630
+ # Not yet. Failing on Travis and this is not a feature yet.
631
+ #
632
+ # There is a spec that has been commented out.
633
+ # @!visibility private
634
+ # TODO Command line tool
635
+ # def app_uia_crash_logs
636
+ # base_dir = app_library_dir
637
+ # if base_dir.nil?
638
+ # nil
639
+ # else
640
+ # dir = File.join(base_dir, 'CrashReporter', 'UIALogs')
641
+ # if Dir.exist?(dir)
642
+ # Dir.glob("#{dir}/*.plist")
643
+ # else
644
+ # nil
645
+ # end
646
+ # end
647
+ # end
648
+ end
@@ -271,9 +271,7 @@ Please update your sources to pass an instance of RunLoop::Xcode))
271
271
  # TODO needs unit tests.
272
272
  def simulator_data_dir_size
273
273
  path = File.join(simulator_root_dir, 'data')
274
- args = ['du', '-m', '-d', '0', path]
275
- hash = xcrun.exec(args)
276
- hash[:out].split(' ').first.to_i
274
+ RunLoop::Directory.size(path, :mb)
277
275
  end
278
276
 
279
277
  # @!visibility private
@@ -316,13 +314,11 @@ Please update your sources to pass an instance of RunLoop::Xcode))
316
314
  current_sha = nil
317
315
  sha_fn = lambda do |data_dir|
318
316
  begin
319
- # Directory.directory_digest has a blocking read. Typically, it
320
- # returns in < 0.3 seconds.
317
+ # Typically, this returns in < 0.3 seconds.
321
318
  Timeout.timeout(2, TimeoutError) do
322
319
  RunLoop::Directory.directory_digest(data_dir)
323
320
  end
324
- rescue => e
325
- RunLoop.log_error(e) if RunLoop::Environment.debug?
321
+ rescue => _
326
322
  SecureRandom.uuid
327
323
  end
328
324
  end
@@ -387,7 +383,11 @@ Please update your sources to pass an instance of RunLoop::Xcode))
387
383
  io.close if io && !io.closed?
388
384
  end
389
385
 
390
- line
386
+ if line
387
+ line.chomp
388
+ else
389
+ line
390
+ end
391
391
  end
392
392
 
393
393
  # @!visibility private