run_loop 1.5.5 → 1.5.6.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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