run_loop 1.3.0 → 1.3.1
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.
- checksums.yaml +4 -4
- data/lib/run_loop/core.rb +114 -32
- data/lib/run_loop/device.rb +28 -0
- data/lib/run_loop/environment.rb +28 -0
- data/lib/run_loop/process_waiter.rb +1 -1
- data/lib/run_loop/sim_control.rb +154 -4
- data/lib/run_loop/simctl/bridge.rb +134 -51
- data/lib/run_loop/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37ad2fc838d709c0132334cf1a24b88822664e9b
|
4
|
+
data.tar.gz: 818e85d9a7480820bdc7737cf99ed1d42b3aef1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12670d8f9a3490d292280e03adaf8d74343ed5af14568e257d8333a2314cb4602c4f566dd0ea55318daaaf1d26e7f207a3db10c0e8d8486bf701eefbab8ac8aa
|
7
|
+
data.tar.gz: 7fff1abf53e185e59ef0b92c8b851b59c0581300ce5d1f1a851b3e0bfeaad2a26df3dbc476141b1deff84ff4a4974e14102f7f44c869e82ba1c2de695cad05be
|
data/lib/run_loop/core.rb
CHANGED
@@ -48,23 +48,6 @@ module RunLoop
|
|
48
48
|
RunLoop::Logging.log_debug(logger, "\n" + message)
|
49
49
|
end
|
50
50
|
|
51
|
-
# @deprecated since 1.0.0
|
52
|
-
# still used extensively in calabash-ios launcher
|
53
|
-
def self.above_or_eql_version?(target_version, xcode_version)
|
54
|
-
if target_version.is_a?(RunLoop::Version)
|
55
|
-
target = target_version
|
56
|
-
else
|
57
|
-
target = RunLoop::Version.new(target_version)
|
58
|
-
end
|
59
|
-
|
60
|
-
if xcode_version.is_a?(RunLoop::Version)
|
61
|
-
xcode = xcode_version
|
62
|
-
else
|
63
|
-
xcode = RunLoop::Version.new(xcode_version)
|
64
|
-
end
|
65
|
-
target >= xcode
|
66
|
-
end
|
67
|
-
|
68
51
|
def self.script_for_key(key)
|
69
52
|
if SCRIPTS[key].nil?
|
70
53
|
return nil
|
@@ -122,6 +105,68 @@ module RunLoop
|
|
122
105
|
end
|
123
106
|
end
|
124
107
|
|
108
|
+
# Prepares the simulator for running.
|
109
|
+
#
|
110
|
+
# 1. enabling accessibility and software keyboard
|
111
|
+
# 2. installing / uninstalling apps
|
112
|
+
# 3. @todo resetting the app sandbox
|
113
|
+
#
|
114
|
+
# `Bridge#launch_simulator` launches the targeted iOS Simulator. The
|
115
|
+
# simulator itself has several async tasks that must be completed before
|
116
|
+
# we start interacting with it. If your simulator ends up in a bad state,
|
117
|
+
# you can increase the post-launch wait time by setting the
|
118
|
+
# `CAL_SIM_POST_LAUNCH_WAIT` environment variable. The default wait time
|
119
|
+
# is 1.0. This was arrived at through testing.
|
120
|
+
def self.prepare_simulator(launch_options, sim_control)
|
121
|
+
|
122
|
+
# Respect option passed from Calabash
|
123
|
+
if launch_options[:relaunch_simulator]
|
124
|
+
sim_control.quit_sim
|
125
|
+
end
|
126
|
+
|
127
|
+
if !sim_control.xctools.xcode_version_gte_6?
|
128
|
+
# Xcode 5.1.1
|
129
|
+
|
130
|
+
# Will quit the simulator!
|
131
|
+
sim_control.enable_accessibility_on_sims({:verbose => false})
|
132
|
+
else
|
133
|
+
|
134
|
+
# CoreSimulator
|
135
|
+
|
136
|
+
udid = launch_options[:udid]
|
137
|
+
device = sim_control.simulators.detect do |sim|
|
138
|
+
sim.udid == udid || sim.instruments_identifier == udid
|
139
|
+
end
|
140
|
+
|
141
|
+
if device.nil?
|
142
|
+
raise "Could not find simulator with name or UDID that matches: '#{udid}'"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Will quit the simulator if it is running.
|
146
|
+
# @todo fix accessibility_enabled? so we don't have to quit the sim
|
147
|
+
# SimControl#accessibility_enabled? is always false during Core#prepare_simulator
|
148
|
+
# https://github.com/calabash/run_loop/issues/167
|
149
|
+
sim_control.ensure_accessibility(device)
|
150
|
+
|
151
|
+
# Will quit the simulator if it is running.
|
152
|
+
# @todo fix software_keyboard_enabled? so we don't have to quit the sim
|
153
|
+
# SimControl#software_keyboard_enabled? is always false during Core#prepare_simulator
|
154
|
+
# https://github.com/calabash/run_loop/issues/167
|
155
|
+
sim_control.ensure_software_keyboard(device)
|
156
|
+
|
157
|
+
# Xcode 6.3 instruments cannot launch an app that is already installed on
|
158
|
+
# iOS 8.3 Simulators. See: https://github.com/calabash/calabash-ios/issues/744
|
159
|
+
if sim_control.xctools.xcode_version_gte_63?
|
160
|
+
app_bundle_path = launch_options[:bundle_dir_or_bundle_id]
|
161
|
+
bridge = RunLoop::Simctl::Bridge.new(device, app_bundle_path)
|
162
|
+
|
163
|
+
if bridge.app_is_installed? && !sim_control.sim_is_running?
|
164
|
+
bridge.launch_simulator
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
125
170
|
def self.run_with_options(options)
|
126
171
|
before = Time.now
|
127
172
|
|
@@ -202,9 +247,8 @@ module RunLoop
|
|
202
247
|
merged_options = options.merge(discovered_options)
|
203
248
|
|
204
249
|
if self.simulator_target?(merged_options, sim_control)
|
205
|
-
# @todo only enable accessibility on the targeted simulator
|
206
|
-
sim_control.enable_accessibility_on_sims({:verbose => false})
|
207
250
|
self.expect_compatible_simulator_architecture(merged_options, sim_control)
|
251
|
+
self.prepare_simulator(merged_options, sim_control)
|
208
252
|
end
|
209
253
|
|
210
254
|
self.log_run_loop_options(merged_options, xctools)
|
@@ -409,11 +453,6 @@ module RunLoop
|
|
409
453
|
return udid, bundle_dir_or_bundle_id
|
410
454
|
end
|
411
455
|
|
412
|
-
# @deprecated 1.0.0 replaced with Xctools#version
|
413
|
-
def self.xcode_version(xctools=RunLoop::XCTools.new)
|
414
|
-
xctools.xcode_version.to_s
|
415
|
-
end
|
416
|
-
|
417
456
|
def self.create_uia_pipe(repl_path)
|
418
457
|
begin
|
419
458
|
Timeout::timeout(5, RunLoop::TimeoutError) do
|
@@ -480,6 +519,11 @@ module RunLoop
|
|
480
519
|
cmd.gsub(backquote,backquote*4)
|
481
520
|
end
|
482
521
|
|
522
|
+
def self.log_instruments_error(msg)
|
523
|
+
$stderr.puts "\033[31m\n\n*** #{msg} ***\n\n\033[0m"
|
524
|
+
$stderr.flush
|
525
|
+
end
|
526
|
+
|
483
527
|
def self.read_response(run_loop, expected_index, empty_file_timeout=10, search_for_property='index')
|
484
528
|
debug_read = RunLoop::Environment.debug_read?
|
485
529
|
|
@@ -499,14 +543,31 @@ module RunLoop
|
|
499
543
|
|
500
544
|
if /AXError: Could not auto-register for pid status change/.match(output)
|
501
545
|
if /kAXErrorServerNotFound/.match(output)
|
502
|
-
|
503
|
-
$stderr.flush
|
546
|
+
self.log_instruments_error('Accessibility is not enabled on device/simulator, please enable it.')
|
504
547
|
end
|
505
548
|
raise RunLoop::TimeoutError.new('AXError: Could not auto-register for pid status change')
|
506
549
|
end
|
550
|
+
|
507
551
|
if /Automation Instrument ran into an exception/.match(output)
|
508
552
|
raise RunLoop::TimeoutError.new('Exception while running script')
|
509
553
|
end
|
554
|
+
|
555
|
+
if /FBSOpenApplicationErrorDomain error/.match(output)
|
556
|
+
msg = "Instruments failed to launch app: 'FBSOpenApplicationErrorDomain error 8"
|
557
|
+
if RunLoop::Environment.debug?
|
558
|
+
self.log_instruments_error(msg)
|
559
|
+
end
|
560
|
+
raise RunLoop::TimeoutError.new(msg)
|
561
|
+
end
|
562
|
+
|
563
|
+
if /Error: Script threw an uncaught JavaScript error: unknown JavaScript exception/.match(output)
|
564
|
+
msg = "Instruments failed to launch: because of an unknown JavaScript exception"
|
565
|
+
if RunLoop::Environment.debug?
|
566
|
+
self.log_instruments_error(msg)
|
567
|
+
end
|
568
|
+
raise RunLoop::TimeoutError.new(msg)
|
569
|
+
end
|
570
|
+
|
510
571
|
index_if_found = output.index(START_DELIMITER)
|
511
572
|
if debug_read
|
512
573
|
puts output.gsub('*', '')
|
@@ -556,11 +617,6 @@ module RunLoop
|
|
556
617
|
result
|
557
618
|
end
|
558
619
|
|
559
|
-
# @deprecated 1.0.5
|
560
|
-
def self.pids_for_run_loop(run_loop, &block)
|
561
|
-
RunLoop::Instruments.new.instruments_pids(&block)
|
562
|
-
end
|
563
|
-
|
564
620
|
def self.automation_template(xctools, candidate = RunLoop::Environment.trace_template)
|
565
621
|
unless candidate && File.exist?(candidate)
|
566
622
|
candidate = default_tracetemplate xctools
|
@@ -608,6 +664,32 @@ module RunLoop
|
|
608
664
|
def self.instruments_pids
|
609
665
|
RunLoop::Instruments.new.instruments_pids
|
610
666
|
end
|
611
|
-
end
|
612
667
|
|
668
|
+
# @deprecated 1.0.0 replaced with Xctools#version
|
669
|
+
def self.xcode_version(xctools=RunLoop::XCTools.new)
|
670
|
+
xctools.xcode_version.to_s
|
671
|
+
end
|
672
|
+
|
673
|
+
# @deprecated since 1.0.0
|
674
|
+
# still used extensively in calabash-ios launcher
|
675
|
+
def self.above_or_eql_version?(target_version, xcode_version)
|
676
|
+
if target_version.is_a?(RunLoop::Version)
|
677
|
+
target = target_version
|
678
|
+
else
|
679
|
+
target = RunLoop::Version.new(target_version)
|
680
|
+
end
|
681
|
+
|
682
|
+
if xcode_version.is_a?(RunLoop::Version)
|
683
|
+
xcode = xcode_version
|
684
|
+
else
|
685
|
+
xcode = RunLoop::Version.new(xcode_version)
|
686
|
+
end
|
687
|
+
target >= xcode
|
688
|
+
end
|
689
|
+
|
690
|
+
# @deprecated 1.0.5
|
691
|
+
def self.pids_for_run_loop(run_loop, &block)
|
692
|
+
RunLoop::Instruments.new.instruments_pids(&block)
|
693
|
+
end
|
694
|
+
end
|
613
695
|
end
|
data/lib/run_loop/device.rb
CHANGED
@@ -5,6 +5,9 @@ module RunLoop
|
|
5
5
|
attr_reader :version
|
6
6
|
attr_reader :udid
|
7
7
|
attr_reader :state
|
8
|
+
attr_reader :simulator_root_dir
|
9
|
+
attr_reader :simulator_accessibility_plist_path
|
10
|
+
attr_reader :simulator_preferences_plist_path
|
8
11
|
|
9
12
|
# Create a new device.
|
10
13
|
#
|
@@ -86,5 +89,30 @@ module RunLoop
|
|
86
89
|
raise 'Finding the instruction set of a device requires a third-party tool like ideviceinfo'
|
87
90
|
end
|
88
91
|
end
|
92
|
+
|
93
|
+
def simulator_root_dir
|
94
|
+
@simulator_root_dir ||= lambda {
|
95
|
+
return nil if physical_device?
|
96
|
+
File.join(CORE_SIMULATOR_DEVICE_DIR, udid)
|
97
|
+
}.call
|
98
|
+
end
|
99
|
+
|
100
|
+
def simulator_accessibility_plist_path
|
101
|
+
@simulator_accessibility_plist_path ||= lambda {
|
102
|
+
return nil if physical_device?
|
103
|
+
File.join(simulator_root_dir, 'data/Library/Preferences/com.apple.Accessibility.plist')
|
104
|
+
}.call
|
105
|
+
end
|
106
|
+
|
107
|
+
def simulator_preferences_plist_path
|
108
|
+
@simulator_preferences_plist_path ||= lambda {
|
109
|
+
return nil if physical_device?
|
110
|
+
File.join(simulator_root_dir, 'data/Library/Preferences/com.apple.Preferences.plist')
|
111
|
+
}.call
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
CORE_SIMULATOR_DEVICE_DIR = File.expand_path('~/Library/Developer/CoreSimulator/Devices')
|
89
117
|
end
|
90
118
|
end
|
data/lib/run_loop/environment.rb
CHANGED
@@ -73,5 +73,33 @@ module RunLoop
|
|
73
73
|
value
|
74
74
|
end
|
75
75
|
end
|
76
|
+
|
77
|
+
# Returns the value of CAL_SIM_POST_LAUNCH_WAIT
|
78
|
+
#
|
79
|
+
# Controls how long to wait _after_ the simulator is opened.
|
80
|
+
#
|
81
|
+
# The default wait time is 1.0. This was arrived at through testing.
|
82
|
+
#
|
83
|
+
# In CoreSimulator environments, the iOS Simulator starts many async
|
84
|
+
# processes that must be allowed to finish before we start operating on the
|
85
|
+
# simulator. Until we find the right combination of processes to wait for,
|
86
|
+
# this variable will give us the opportunity to control how long we wait.
|
87
|
+
#
|
88
|
+
# Essential for managed envs like Travis + Jenkins and on slower machines.
|
89
|
+
def self.sim_post_launch_wait
|
90
|
+
value = ENV['CAL_SIM_POST_LAUNCH_WAIT']
|
91
|
+
float = nil
|
92
|
+
begin
|
93
|
+
float = value.to_f
|
94
|
+
rescue NoMethodError => _
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
if float.nil? || float == 0.0
|
99
|
+
nil
|
100
|
+
else
|
101
|
+
float
|
102
|
+
end
|
103
|
+
end
|
76
104
|
end
|
77
105
|
end
|
@@ -13,7 +13,7 @@ module RunLoop
|
|
13
13
|
# Collect a list of Integer pids.
|
14
14
|
# @return [Array<Integer>] An array of integer pids for the `process_name`
|
15
15
|
def pids
|
16
|
-
process_info = `ps x -o pid,comm | grep -v grep | grep #{process_name}`
|
16
|
+
process_info = `ps x -o pid,comm | grep -v grep | grep '#{process_name}'`
|
17
17
|
process_array = process_info.split("\n")
|
18
18
|
process_array.map { |process| process.split(' ').first.strip.to_i }
|
19
19
|
end
|
data/lib/run_loop/sim_control.rb
CHANGED
@@ -82,7 +82,7 @@ module RunLoop
|
|
82
82
|
# @todo Consider migrating apple script call to xctools.
|
83
83
|
def launch_sim(opts={})
|
84
84
|
unless sim_is_running?
|
85
|
-
default_opts = {:post_launch_wait => 2.0,
|
85
|
+
default_opts = {:post_launch_wait => RunLoop::Environment.sim_post_launch_wait || 2.0,
|
86
86
|
:hide_after => false}
|
87
87
|
merged_opts = default_opts.merge(opts)
|
88
88
|
`xcrun open -a "#{sim_app_path}"`
|
@@ -107,7 +107,7 @@ module RunLoop
|
|
107
107
|
# tired of your editor losing focus. :)
|
108
108
|
def relaunch_sim(opts={})
|
109
109
|
default_opts = {:post_quit_wait => 1.0,
|
110
|
-
:post_launch_wait => 2.0,
|
110
|
+
:post_launch_wait => RunLoop::Environment.sim_post_launch_wait || 2.0,
|
111
111
|
:hide_after => false}
|
112
112
|
merged_opts = default_opts.merge(opts)
|
113
113
|
quit_sim(merged_opts)
|
@@ -199,7 +199,7 @@ module RunLoop
|
|
199
199
|
# **NOTE:** This option is ignored in Xcode < 6.
|
200
200
|
def reset_sim_content_and_settings(opts={})
|
201
201
|
default_opts = {:post_quit_wait => 1.0,
|
202
|
-
:post_launch_wait => 3.0,
|
202
|
+
:post_launch_wait => RunLoop::Environment.sim_post_launch_wait || 3.0,
|
203
203
|
:hide_after => false,
|
204
204
|
:sim_udid => nil}
|
205
205
|
merged_opts = default_opts.merge(opts)
|
@@ -333,8 +333,149 @@ module RunLoop
|
|
333
333
|
end
|
334
334
|
end
|
335
335
|
|
336
|
-
|
336
|
+
def accessibility_enabled?(device)
|
337
|
+
plist = device.simulator_accessibility_plist_path
|
338
|
+
return false unless File.exist?(plist)
|
339
|
+
|
340
|
+
if device.version >= RunLoop::Version.new('8.0')
|
341
|
+
plist_hash = SDK_80_ACCESSIBILITY_PROPERTIES_HASH
|
342
|
+
else
|
343
|
+
plist_hash = SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH
|
344
|
+
end
|
345
|
+
|
346
|
+
plist_hash.each do |_, details|
|
347
|
+
key = details[:key]
|
348
|
+
value = details[:value]
|
349
|
+
|
350
|
+
unless pbuddy.plist_read(key, plist) == "#{value}"
|
351
|
+
return false
|
352
|
+
end
|
353
|
+
end
|
354
|
+
true
|
355
|
+
end
|
356
|
+
|
357
|
+
def ensure_accessibility(device)
|
358
|
+
if accessibility_enabled?(device)
|
359
|
+
true
|
360
|
+
else
|
361
|
+
enable_accessibility(device)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def enable_accessibility(device)
|
366
|
+
debug_logging = RunLoop::Environment.debug?
|
367
|
+
|
368
|
+
quit_sim
|
369
|
+
|
370
|
+
plist_path = device.simulator_accessibility_plist_path
|
371
|
+
|
372
|
+
if device.version >= RunLoop::Version.new('8.0')
|
373
|
+
plist_hash = SDK_80_ACCESSIBILITY_PROPERTIES_HASH
|
374
|
+
else
|
375
|
+
plist_hash = SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH
|
376
|
+
end
|
377
|
+
|
378
|
+
unless File.exist? plist_path
|
379
|
+
preferences_dir = File.join(device.simulator_root_dir, 'data/Library/Preferences')
|
380
|
+
FileUtils.mkdir_p(preferences_dir)
|
381
|
+
plist = CFPropertyList::List.new
|
382
|
+
data = {}
|
383
|
+
plist.value = CFPropertyList.guess(data)
|
384
|
+
plist.save(plist_path, CFPropertyList::List::FORMAT_BINARY)
|
385
|
+
end
|
386
|
+
|
387
|
+
msgs = []
|
388
|
+
|
389
|
+
successes = plist_hash.map do |hash_key, settings|
|
390
|
+
success = pbuddy.plist_set(settings[:key], settings[:type], settings[:value], plist_path)
|
391
|
+
unless success
|
392
|
+
if debug_logging
|
393
|
+
if settings[:type] == 'bool'
|
394
|
+
value = settings[:value] ? 'YES' : 'NO'
|
395
|
+
else
|
396
|
+
value = settings[:value]
|
397
|
+
end
|
398
|
+
msgs << "could not set #{hash_key} => '#{settings[:key]}' to #{value}"
|
399
|
+
end
|
400
|
+
end
|
401
|
+
success
|
402
|
+
end
|
403
|
+
|
404
|
+
if successes.all?
|
405
|
+
true
|
406
|
+
else
|
407
|
+
return false, msgs
|
408
|
+
end
|
409
|
+
end
|
337
410
|
|
411
|
+
def software_keyboard_enabled?(device)
|
412
|
+
unless xcode_version_gte_51?
|
413
|
+
raise RuntimeError, 'Keyboard enabling is only available on Xcode >= 6'
|
414
|
+
end
|
415
|
+
|
416
|
+
plist = device.simulator_preferences_plist_path
|
417
|
+
return false unless File.exist?(plist)
|
418
|
+
|
419
|
+
CORE_SIMULATOR_KEYBOARD_PROPERTIES_HASH.each do |_, details|
|
420
|
+
key = details[:key]
|
421
|
+
value = details[:value]
|
422
|
+
|
423
|
+
unless pbuddy.plist_read(key, plist) == "#{value}"
|
424
|
+
return false
|
425
|
+
end
|
426
|
+
end
|
427
|
+
true
|
428
|
+
end
|
429
|
+
|
430
|
+
def ensure_software_keyboard(device)
|
431
|
+
if software_keyboard_enabled?(device)
|
432
|
+
true
|
433
|
+
else
|
434
|
+
enable_software_keyboard(device)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def enable_software_keyboard(device)
|
439
|
+
debug_logging = RunLoop::Environment.debug?
|
440
|
+
|
441
|
+
quit_sim
|
442
|
+
|
443
|
+
plist_path = device.simulator_preferences_plist_path
|
444
|
+
|
445
|
+
unless File.exist? plist_path
|
446
|
+
preferences_dir = File.join(device.simulator_root_dir, 'data/Library/Preferences')
|
447
|
+
FileUtils.mkdir_p(preferences_dir)
|
448
|
+
plist = CFPropertyList::List.new
|
449
|
+
data = {}
|
450
|
+
plist.value = CFPropertyList.guess(data)
|
451
|
+
plist.save(plist_path, CFPropertyList::List::FORMAT_BINARY)
|
452
|
+
end
|
453
|
+
|
454
|
+
msgs = []
|
455
|
+
|
456
|
+
successes = CORE_SIMULATOR_KEYBOARD_PROPERTIES_HASH.map do |hash_key, settings|
|
457
|
+
success = pbuddy.plist_set(settings[:key], settings[:type], settings[:value], plist_path)
|
458
|
+
unless success
|
459
|
+
if debug_logging
|
460
|
+
if settings[:type] == 'bool'
|
461
|
+
value = settings[:value] ? 'YES' : 'NO'
|
462
|
+
else
|
463
|
+
value = settings[:value]
|
464
|
+
end
|
465
|
+
msgs << "could not set #{hash_key} => '#{settings[:key]}' to #{value}"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
success
|
469
|
+
end
|
470
|
+
|
471
|
+
if successes.all?
|
472
|
+
true
|
473
|
+
else
|
474
|
+
return false, msgs
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
private
|
338
479
|
|
339
480
|
# @!visibility private
|
340
481
|
# The list of possible SDKs for 5.0 <= Xcode < 6.0
|
@@ -449,6 +590,15 @@ module RunLoop
|
|
449
590
|
# and parsing the output of `simctl list sessions`.
|
450
591
|
XCODE_6_SIM_UDID_REGEX = /[A-F0-9]{8}-([A-F0-9]{4}-){3}[A-F0-9]{12}/.freeze
|
451
592
|
|
593
|
+
CORE_SIMULATOR_KEYBOARD_PROPERTIES_HASH =
|
594
|
+
{
|
595
|
+
:automatic_minimization => {
|
596
|
+
:key => 'AutomaticMinimizationEnabled',
|
597
|
+
:value => 0,
|
598
|
+
:type => 'integer'
|
599
|
+
}
|
600
|
+
}
|
601
|
+
|
452
602
|
# @!visibility private
|
453
603
|
# Returns the current Simulator pid.
|
454
604
|
#
|
@@ -1,73 +1,96 @@
|
|
1
1
|
module RunLoop::Simctl
|
2
2
|
|
3
|
+
class SimctlError < StandardError
|
4
|
+
|
5
|
+
end
|
6
|
+
|
3
7
|
# @!visibility private
|
4
8
|
# This is not a public API. You have been warned.
|
5
9
|
#
|
6
|
-
# Proof of concept for using simctl to install and launch an app on a device.
|
7
|
-
#
|
8
|
-
# Do not use this in production code.
|
9
|
-
#
|
10
|
-
# TODO Rename this class.
|
11
10
|
# TODO Some code is duplicated from sim_control.rb
|
12
|
-
# TODO Uninstall
|
13
11
|
# TODO Reinstall if checksum does not match.
|
14
12
|
# TODO Analyze terminate_core_simulator_processes
|
15
13
|
class Bridge
|
16
14
|
|
17
15
|
attr_reader :device
|
18
|
-
attr_reader :app_bundle_path
|
19
16
|
attr_reader :app
|
17
|
+
attr_reader :sim_control
|
20
18
|
|
21
19
|
def initialize(device, app_bundle_path)
|
22
|
-
|
20
|
+
|
23
21
|
@sim_control = RunLoop::SimControl.new
|
24
|
-
@
|
22
|
+
@path_to_ios_sim_app_bundle = lambda {
|
25
23
|
dev_dir = @sim_control.xctools.xcode_developer_dir
|
26
24
|
"#{dev_dir}/Applications/iOS Simulator.app"
|
27
25
|
}.call
|
28
26
|
|
29
|
-
@app_bundle_path = app_bundle_path
|
30
27
|
@app = RunLoop::App.new(app_bundle_path)
|
31
|
-
RunLoop::SimControl.terminate_all_sims
|
32
|
-
shutdown
|
33
|
-
terminate_core_simulator_processes
|
34
|
-
end
|
35
|
-
|
36
|
-
def udid
|
37
|
-
@udid ||= device.udid
|
38
|
-
end
|
39
28
|
|
40
|
-
|
41
|
-
|
42
|
-
|
29
|
+
unless @app.valid?
|
30
|
+
raise "Could not recreate a valid app from '#{app_bundle_path}'"
|
31
|
+
end
|
43
32
|
|
44
|
-
|
45
|
-
app.bundle_identifier
|
46
|
-
end
|
33
|
+
@device = device
|
47
34
|
|
48
|
-
|
49
|
-
|
35
|
+
# It may seem weird to do this in the initialize, but you cannot make
|
36
|
+
# simctl calls successfully unless the simulator is:
|
37
|
+
# 1. closed
|
38
|
+
# 2. the device you are trying to operate on is Shutdown
|
39
|
+
# 3. the CoreSimulator processes are terminated
|
40
|
+
RunLoop::SimControl.terminate_all_sims
|
41
|
+
shutdown
|
42
|
+
terminate_core_simulator_processes
|
50
43
|
end
|
51
44
|
|
52
45
|
def simulator_app_dir
|
53
46
|
@simulator_app_dir ||= lambda {
|
54
47
|
device_dir = File.expand_path('~/Library/Developer/CoreSimulator/Devices')
|
55
48
|
if device.version < RunLoop::Version.new('8.0')
|
56
|
-
File.join(device_dir, udid, 'data', 'Applications')
|
49
|
+
File.join(device_dir, device.udid, 'data', 'Applications')
|
57
50
|
else
|
58
|
-
File.join(device_dir, udid, 'data', 'Containers', 'Bundle', 'Application')
|
51
|
+
File.join(device_dir, device.udid, 'data', 'Containers', 'Bundle', 'Application')
|
59
52
|
end
|
60
53
|
}.call
|
61
54
|
end
|
62
55
|
|
63
|
-
def update_device_state
|
64
|
-
|
65
|
-
|
56
|
+
def update_device_state(options={})
|
57
|
+
merged_options = UPDATE_DEVICE_STATE_OPTS.merge(options)
|
58
|
+
debug_logging = RunLoop::Environment.debug?
|
59
|
+
|
60
|
+
interval = merged_options[:interval]
|
61
|
+
tries = merged_options[:tries]
|
62
|
+
|
63
|
+
on_retry = Proc.new do |_, try, elapsed_time, next_interval|
|
64
|
+
if debug_logging
|
65
|
+
# Retriable 2.0
|
66
|
+
if elapsed_time && next_interval
|
67
|
+
puts "Updating device state attempt #{try} failed in '#{elapsed_time}'; will retry in '#{next_interval}'"
|
68
|
+
else
|
69
|
+
puts "Updating device state attempt #{try} failed; will retry in #{interval}"
|
70
|
+
end
|
71
|
+
end
|
66
72
|
end
|
67
|
-
|
68
|
-
|
73
|
+
|
74
|
+
retry_opts = RunLoop::RetryOpts.tries_and_interval(tries, interval,
|
75
|
+
{:on_retry => on_retry,
|
76
|
+
:on => [SimctlError]
|
77
|
+
})
|
78
|
+
matching_device = nil
|
79
|
+
|
80
|
+
Retriable.retriable(retry_opts) do
|
81
|
+
matching_device = fetch_matching_device
|
82
|
+
|
83
|
+
unless matching_device
|
84
|
+
raise "simctl could not find device with '#{device.udid}'"
|
85
|
+
end
|
86
|
+
|
87
|
+
if matching_device.state == nil || matching_device.state == ''
|
88
|
+
raise SimctlError, "Could not find the state of the device with #{device.udid}"
|
89
|
+
end
|
69
90
|
end
|
70
|
-
|
91
|
+
|
92
|
+
# Device#state is immutable
|
93
|
+
@device = matching_device
|
71
94
|
@device.state
|
72
95
|
end
|
73
96
|
|
@@ -86,7 +109,6 @@ module RunLoop::Simctl
|
|
86
109
|
end
|
87
110
|
end
|
88
111
|
|
89
|
-
|
90
112
|
def wait_for_device_state(target_state)
|
91
113
|
return true if update_device_state == target_state
|
92
114
|
|
@@ -112,7 +134,7 @@ module RunLoop::Simctl
|
|
112
134
|
sim_app_dir = simulator_app_dir
|
113
135
|
return false if !File.exist?(sim_app_dir)
|
114
136
|
app_path = Dir.glob("#{sim_app_dir}/**/*.app").detect do |path|
|
115
|
-
RunLoop::App.new(path).bundle_identifier == bundle_identifier
|
137
|
+
RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
|
116
138
|
end
|
117
139
|
|
118
140
|
!app_path.nil?
|
@@ -132,10 +154,33 @@ module RunLoop::Simctl
|
|
132
154
|
sleep delay
|
133
155
|
end
|
134
156
|
|
135
|
-
puts "Waited for #{timeout} seconds for '#{bundle_identifier}' to install."
|
157
|
+
puts "Waited for #{timeout} seconds for '#{app.bundle_identifier}' to install."
|
136
158
|
|
137
159
|
unless is_installed
|
138
|
-
raise "Expected app to be installed on #{
|
160
|
+
raise "Expected app to be installed on #{device.instruments_identifier}"
|
161
|
+
end
|
162
|
+
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
def wait_for_app_uninstall
|
167
|
+
return true unless app_is_installed?
|
168
|
+
|
169
|
+
now = Time.now
|
170
|
+
timeout = WAIT_FOR_APP_INSTALL_OPTS[:timeout]
|
171
|
+
poll_until = now + timeout
|
172
|
+
delay = WAIT_FOR_APP_INSTALL_OPTS[:interval]
|
173
|
+
not_installed = false
|
174
|
+
while Time.now < poll_until
|
175
|
+
not_installed = !app_is_installed?
|
176
|
+
break if not_installed
|
177
|
+
sleep delay
|
178
|
+
end
|
179
|
+
|
180
|
+
puts "Waited for #{timeout} seconds for '#{app.bundle_identifier}' to uninstall."
|
181
|
+
|
182
|
+
unless not_installed
|
183
|
+
raise "Expected app to be installed on #{device.instruments_identifier}"
|
139
184
|
end
|
140
185
|
|
141
186
|
true
|
@@ -145,15 +190,15 @@ module RunLoop::Simctl
|
|
145
190
|
return true if update_device_state == 'Shutdown'
|
146
191
|
|
147
192
|
if device.state != 'Booted'
|
148
|
-
raise "Cannot handle state '#{device.state}' for #{
|
193
|
+
raise "Cannot handle state '#{device.state}' for #{device.instruments_identifier}"
|
149
194
|
end
|
150
195
|
|
151
|
-
args = "simctl shutdown #{udid}".split(' ')
|
196
|
+
args = "simctl shutdown #{device.udid}".split(' ')
|
152
197
|
Open3.popen3('xcrun', *args) do |_, _, stderr, status|
|
153
198
|
err = stderr.read.strip
|
154
199
|
exit_status = status.value.exitstatus
|
155
200
|
if exit_status != 0
|
156
|
-
raise "Could not shutdown #{
|
201
|
+
raise "Could not shutdown #{device.instruments_identifier}: #{exit_status} => '#{err}'"
|
157
202
|
end
|
158
203
|
end
|
159
204
|
wait_for_device_state('Shutdown')
|
@@ -163,15 +208,15 @@ module RunLoop::Simctl
|
|
163
208
|
return true if update_device_state == 'Booted'
|
164
209
|
|
165
210
|
if device.state != 'Shutdown'
|
166
|
-
raise "Cannot handle state '#{device.state}' for #{
|
211
|
+
raise "Cannot handle state '#{device.state}' for #{device.instruments_identifier}"
|
167
212
|
end
|
168
213
|
|
169
|
-
args = "simctl boot #{udid}".split(' ')
|
214
|
+
args = "simctl boot #{device.udid}".split(' ')
|
170
215
|
Open3.popen3('xcrun', *args) do |_, _, stderr, status|
|
171
216
|
err = stderr.read.strip
|
172
217
|
exit_status = status.value.exitstatus
|
173
218
|
if exit_status != 0
|
174
|
-
raise "Could not boot #{
|
219
|
+
raise "Could not boot #{device.instruments_identifier}: #{exit_status} => '#{err}'"
|
175
220
|
end
|
176
221
|
end
|
177
222
|
wait_for_device_state('Booted')
|
@@ -182,12 +227,12 @@ module RunLoop::Simctl
|
|
182
227
|
|
183
228
|
boot
|
184
229
|
|
185
|
-
args = "simctl install #{udid} #{app.path}".split(' ')
|
230
|
+
args = "simctl install #{device.udid} #{app.path}".split(' ')
|
186
231
|
Open3.popen3('xcrun', *args) do |_, _, stderr, process_status|
|
187
232
|
err = stderr.read.strip
|
188
233
|
exit_status = process_status.value.exitstatus
|
189
234
|
if exit_status != 0
|
190
|
-
raise "Could not install '#{bundle_identifier}': #{exit_status} => '#{err}'."
|
235
|
+
raise "Could not install '#{app.bundle_identifier}': #{exit_status} => '#{err}'."
|
191
236
|
end
|
192
237
|
end
|
193
238
|
|
@@ -195,9 +240,33 @@ module RunLoop::Simctl
|
|
195
240
|
shutdown
|
196
241
|
end
|
197
242
|
|
243
|
+
def uninstall
|
244
|
+
return true unless app_is_installed?
|
245
|
+
|
246
|
+
boot
|
247
|
+
|
248
|
+
args = "simctl uninstall #{device.udid} #{app.bundle_identifier}".split(' ')
|
249
|
+
Open3.popen3('xcrun', *args) do |_, _, stderr, process_status|
|
250
|
+
err = stderr.read.strip
|
251
|
+
exit_status = process_status.value.exitstatus
|
252
|
+
if exit_status != 0
|
253
|
+
raise "Could not uninstall '#{app.bundle_identifier}': #{exit_status} => '#{err}'."
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
wait_for_app_uninstall
|
258
|
+
shutdown
|
259
|
+
end
|
260
|
+
|
198
261
|
def launch_simulator
|
199
|
-
|
200
|
-
|
262
|
+
args = ['open', '-a', @path_to_ios_sim_app_bundle, '--args', '-CurrentDeviceUDID', device.udid]
|
263
|
+
pid = spawn('xcrun', *args)
|
264
|
+
Process.detach(pid)
|
265
|
+
RunLoop::ProcessWaiter.new('CoreSimulatorBridge', WAIT_FOR_APP_LAUNCH_OPTS).wait_for_any
|
266
|
+
RunLoop::ProcessWaiter.new('iOS Simulator', WAIT_FOR_APP_LAUNCH_OPTS).wait_for_any
|
267
|
+
RunLoop::ProcessWaiter.new('SimulatorBridge', WAIT_FOR_APP_LAUNCH_OPTS).wait_for_any
|
268
|
+
wait_for_device_state 'Booted'
|
269
|
+
sleep(SIM_POST_LAUNCH_WAIT)
|
201
270
|
end
|
202
271
|
|
203
272
|
def launch
|
@@ -205,16 +274,16 @@ module RunLoop::Simctl
|
|
205
274
|
install
|
206
275
|
launch_simulator
|
207
276
|
|
208
|
-
args = "simctl launch #{udid} #{bundle_identifier}".split(' ')
|
277
|
+
args = "simctl launch #{device.udid} #{app.bundle_identifier}".split(' ')
|
209
278
|
Open3.popen3('xcrun', *args) do |_, _, stderr, process_status|
|
210
279
|
err = stderr.read.strip
|
211
280
|
exit_status = process_status.value.exitstatus
|
212
281
|
unless exit_status == 0
|
213
|
-
raise "Could not simctl launch '#{bundle_identifier}' on '#{
|
282
|
+
raise "Could not simctl launch '#{app.bundle_identifier}' on '#{device.instruments_identifier}': #{exit_status} => '#{err}'"
|
214
283
|
end
|
215
284
|
end
|
216
285
|
|
217
|
-
RunLoop::ProcessWaiter.new(executable_name, WAIT_FOR_APP_LAUNCH_OPTS).wait_for_any
|
286
|
+
RunLoop::ProcessWaiter.new(app.executable_name, WAIT_FOR_APP_LAUNCH_OPTS).wait_for_any
|
218
287
|
true
|
219
288
|
end
|
220
289
|
|
@@ -238,5 +307,19 @@ module RunLoop::Simctl
|
|
238
307
|
raise_on_timeout: true
|
239
308
|
}
|
240
309
|
|
310
|
+
UPDATE_DEVICE_STATE_OPTS =
|
311
|
+
{
|
312
|
+
:tries => 100,
|
313
|
+
:interval => 0.1
|
314
|
+
}
|
315
|
+
|
316
|
+
SIM_POST_LAUNCH_WAIT = RunLoop::Environment.sim_post_launch_wait || 1.0
|
317
|
+
|
318
|
+
# @!visibility private
|
319
|
+
def fetch_matching_device
|
320
|
+
sim_control.simulators.detect do |sim|
|
321
|
+
sim.udid == device.udid
|
322
|
+
end
|
323
|
+
end
|
241
324
|
end
|
242
325
|
end
|
data/lib/run_loop/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: run_loop
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Karl Krukow
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|