run_loop 1.2.7 → 1.2.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/run_loop.rb +173 -0
- data/lib/run_loop/core.rb +49 -231
- data/lib/run_loop/environment.rb +4 -0
- data/lib/run_loop/fifo.rb +40 -0
- data/lib/run_loop/instruments.rb +2 -4
- data/lib/run_loop/lldb.rb +1 -3
- data/lib/run_loop/logging.rb +36 -0
- data/lib/run_loop/process_waiter.rb +38 -0
- data/lib/run_loop/sim_control.rb +1 -0
- data/lib/run_loop/version.rb +1 -1
- data/scripts/calabash-uia-min.js +7 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 253c24d7121ffc84824a1d3e7a5e3c79d14dfcd8
|
4
|
+
data.tar.gz: d22bc4efbace3cbcd82e3ab023515984beff9618
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 716a2225ff8d2ff978b28852d803c84ba97ac05a75a0ce74a714f910ef35317f9322f4818c8812f88b72bafcda6542e3df1a82c87dfe73137e8737a687aeb61f
|
7
|
+
data.tar.gz: 2750db0951ae73dd859fac716b138ffd02972e42f2e1aec600f9faf4bb10addf7e19d334488fed3f59f3476aacbbb87f080bf8eca81a4263166fa2f3c58262e6
|
data/lib/run_loop.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'run_loop/environment'
|
2
|
+
require 'run_loop/logging'
|
2
3
|
require 'run_loop/process_terminator'
|
3
4
|
require 'run_loop/process_waiter'
|
4
5
|
require 'run_loop/lldb'
|
5
6
|
require 'run_loop/dylib_injector'
|
7
|
+
require 'run_loop/fifo'
|
6
8
|
require 'run_loop/core'
|
7
9
|
require 'run_loop/version'
|
8
10
|
require 'run_loop/xctools'
|
@@ -15,3 +17,174 @@ require 'run_loop/lipo'
|
|
15
17
|
require 'run_loop/host_cache'
|
16
18
|
require 'run_loop/monkey_patch'
|
17
19
|
require 'run_loop/simctl/bridge'
|
20
|
+
|
21
|
+
module RunLoop
|
22
|
+
|
23
|
+
class TimeoutError < RuntimeError
|
24
|
+
end
|
25
|
+
|
26
|
+
class WriteFailedError < RuntimeError
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.run(options={})
|
30
|
+
|
31
|
+
if RunLoop::Instruments.new.instruments_app_running?
|
32
|
+
msg =
|
33
|
+
[
|
34
|
+
"Please quit the Instruments.app.",
|
35
|
+
"If Instruments.app is open, the instruments command line",
|
36
|
+
"tool cannot take control of your application."
|
37
|
+
]
|
38
|
+
raise msg.join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
uia_strategy = options[:uia_strategy]
|
42
|
+
if options[:script]
|
43
|
+
script = validate_script(options[:script])
|
44
|
+
else
|
45
|
+
if uia_strategy
|
46
|
+
script = default_script_for_uia_strategy(uia_strategy)
|
47
|
+
else
|
48
|
+
if options[:calabash_lite]
|
49
|
+
uia_strategy = :host
|
50
|
+
script = Core.script_for_key(:run_loop_host)
|
51
|
+
else
|
52
|
+
uia_strategy = :preferences
|
53
|
+
script = default_script_for_uia_strategy(uia_strategy)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
# At this point, 'script' has been chosen, but uia_strategy might not
|
58
|
+
unless uia_strategy
|
59
|
+
desired_script = options[:script]
|
60
|
+
if desired_script.is_a?(String) #custom path to script
|
61
|
+
uia_strategy = :host
|
62
|
+
elsif desired_script == :run_loop_host
|
63
|
+
uia_strategy = :host
|
64
|
+
elsif desired_script == :run_loop_fast_uia
|
65
|
+
uia_strategy = :preferences
|
66
|
+
elsif desired_script == :run_loop_shared_element
|
67
|
+
uia_strategy = :shared_element
|
68
|
+
else
|
69
|
+
raise "Inconsistent state: desired script #{desired_script} has not uia_strategy"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# At this point script and uia_strategy selected
|
73
|
+
|
74
|
+
options[:script] = script
|
75
|
+
options[:uia_strategy] = uia_strategy
|
76
|
+
|
77
|
+
Core.run_with_options(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.send_command(run_loop, cmd, options={timeout: 60}, num_retries=0, last_error=nil)
|
81
|
+
if num_retries > 3
|
82
|
+
if last_error
|
83
|
+
raise last_error
|
84
|
+
else
|
85
|
+
raise "Max retries exceeded #{num_retries} > 3. No error recorded."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if options.is_a?(Numeric)
|
90
|
+
options = {timeout: options}
|
91
|
+
end
|
92
|
+
|
93
|
+
if not cmd.is_a?(String)
|
94
|
+
raise "Illegal command #{cmd} (must be a string)"
|
95
|
+
end
|
96
|
+
|
97
|
+
if not options.is_a?(Hash)
|
98
|
+
raise "Illegal options #{options} (must be a Hash (or number for compatibility))"
|
99
|
+
end
|
100
|
+
|
101
|
+
timeout = options[:timeout] || 60
|
102
|
+
logger = options[:logger]
|
103
|
+
interrupt_retry_timeout = options[:interrupt_retry_timeout] || 25
|
104
|
+
|
105
|
+
expected_index = run_loop[:index]
|
106
|
+
result = nil
|
107
|
+
begin
|
108
|
+
expected_index = Core.write_request(run_loop, cmd, logger)
|
109
|
+
rescue RunLoop::WriteFailedError, Errno::EINTR => write_error
|
110
|
+
# Attempt recover from interrupt by attempting to read result (assuming write went OK)
|
111
|
+
# or retry if attempted read result fails
|
112
|
+
run_loop[:index] = expected_index # restore expected index in case it changed
|
113
|
+
log_info(logger, "Core.write_request failed: #{write_error}. Attempting recovery...")
|
114
|
+
log_info(logger, "Attempting read in case the request was received... Please wait (#{interrupt_retry_timeout})...")
|
115
|
+
begin
|
116
|
+
Timeout::timeout(interrupt_retry_timeout, TimeoutError) do
|
117
|
+
result = Core.read_response(run_loop, expected_index)
|
118
|
+
end
|
119
|
+
# Update run_loop expected index since we succeeded in reading the index
|
120
|
+
run_loop[:index] = expected_index + 1
|
121
|
+
log_info(logger, "Did read response for interrupted request of index #{expected_index}... Proceeding.")
|
122
|
+
return result
|
123
|
+
rescue TimeoutError => _
|
124
|
+
log_info(logger, "Read did not result in a response for index #{expected_index}... Retrying send_command...")
|
125
|
+
return send_command(run_loop, cmd, options, num_retries+1, write_error)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
begin
|
131
|
+
Timeout::timeout(timeout, TimeoutError) do
|
132
|
+
result = Core.read_response(run_loop, expected_index)
|
133
|
+
end
|
134
|
+
rescue TimeoutError => _
|
135
|
+
raise TimeoutError, "Time out waiting for UIAutomation run-loop for command #{cmd}. Waiting for index:#{expected_index}"
|
136
|
+
end
|
137
|
+
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.stop(run_loop, out=Dir.pwd)
|
142
|
+
return if run_loop.nil?
|
143
|
+
results_dir = run_loop[:results_dir]
|
144
|
+
dest = out
|
145
|
+
|
146
|
+
RunLoop::Instruments.new.kill_instruments
|
147
|
+
|
148
|
+
FileUtils.mkdir_p(dest)
|
149
|
+
if results_dir
|
150
|
+
pngs = Dir.glob(File.join(results_dir, 'Run 1', '*.png'))
|
151
|
+
else
|
152
|
+
pngs = []
|
153
|
+
end
|
154
|
+
FileUtils.cp(pngs, dest) if pngs and pngs.length > 0
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.default_script_for_uia_strategy(uia_strategy)
|
158
|
+
case uia_strategy
|
159
|
+
when :preferences
|
160
|
+
Core.script_for_key(:run_loop_fast_uia)
|
161
|
+
when :host
|
162
|
+
Core.script_for_key(:run_loop_host)
|
163
|
+
when :shared_element
|
164
|
+
Core.script_for_key(:run_loop_shared_element)
|
165
|
+
else
|
166
|
+
Core.script_for_key(:run_loop_basic)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.validate_script(script)
|
171
|
+
if script.is_a?(String)
|
172
|
+
unless File.exist?(script)
|
173
|
+
raise "Unable to find file: #{script}"
|
174
|
+
end
|
175
|
+
elsif script.is_a?(Symbol)
|
176
|
+
script = Core.script_for_key(script)
|
177
|
+
unless script
|
178
|
+
raise "Unknown script for symbol: #{script}. Options: #{Core::SCRIPTS.keys.join(', ')}"
|
179
|
+
end
|
180
|
+
else
|
181
|
+
raise "Script must be a symbol or path: #{script}"
|
182
|
+
end
|
183
|
+
script
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.log_info(*args)
|
187
|
+
RunLoop::Logging.log_info(*args)
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
data/lib/run_loop/core.rb
CHANGED
@@ -8,12 +8,6 @@ require 'ap'
|
|
8
8
|
|
9
9
|
module RunLoop
|
10
10
|
|
11
|
-
class TimeoutError < RuntimeError
|
12
|
-
end
|
13
|
-
|
14
|
-
class WriteFailedError < RuntimeError
|
15
|
-
end
|
16
|
-
|
17
11
|
module Core
|
18
12
|
|
19
13
|
START_DELIMITER = "OUTPUT_JSON:\n"
|
@@ -36,7 +30,7 @@ module RunLoop
|
|
36
30
|
end
|
37
31
|
|
38
32
|
def self.log_run_loop_options(options, xctools)
|
39
|
-
return unless
|
33
|
+
return unless RunLoop::Environment.debug?
|
40
34
|
# Ignore :sim_control b/c it is a ruby object; printing is not useful.
|
41
35
|
ignored_keys = [:sim_control]
|
42
36
|
options_to_log = {}
|
@@ -49,7 +43,9 @@ module RunLoop
|
|
49
43
|
# RunLoop::Version overrides '=='
|
50
44
|
options_to_log[:xcode] = xctools.xcode_version.to_s
|
51
45
|
options_to_log[:xcode_path] = xctools.xcode_developer_dir
|
52
|
-
|
46
|
+
message = options_to_log.ai({:sort_keys => true})
|
47
|
+
logger = options[:logger]
|
48
|
+
RunLoop::Logging.log_debug(logger, "\n" + message)
|
53
49
|
end
|
54
50
|
|
55
51
|
# @deprecated since 1.0.0
|
@@ -78,10 +74,10 @@ module RunLoop
|
|
78
74
|
|
79
75
|
def self.detect_connected_device
|
80
76
|
begin
|
81
|
-
Timeout::timeout(1, TimeoutError) do
|
77
|
+
Timeout::timeout(1, RunLoop::TimeoutError) do
|
82
78
|
return `#{File.join(scripts_path, 'udidetect')}`.chomp
|
83
79
|
end
|
84
|
-
rescue TimeoutError => _
|
80
|
+
rescue RunLoop::TimeoutError => _
|
85
81
|
`killall udidetect &> /dev/null`
|
86
82
|
end
|
87
83
|
nil
|
@@ -104,6 +100,7 @@ module RunLoop
|
|
104
100
|
# @raise [RunLoop::IncompatibleArchitecture] Raises an error if the
|
105
101
|
# application binary is not compatible with the target simulator.
|
106
102
|
def self.expect_compatible_simulator_architecture(launch_options, sim_control)
|
103
|
+
logger = launch_options[:logger]
|
107
104
|
if sim_control.xcode_version_gte_6?
|
108
105
|
sim_identifier = launch_options[:udid]
|
109
106
|
simulator = sim_control.simulators.find do |simulator|
|
@@ -117,14 +114,10 @@ module RunLoop
|
|
117
114
|
|
118
115
|
lipo = RunLoop::Lipo.new(launch_options[:bundle_dir_or_bundle_id])
|
119
116
|
lipo.expect_compatible_arch(simulator)
|
120
|
-
|
121
|
-
puts "Simulator instruction set '#{simulator.instruction_set}' is compatible with #{lipo.info}"
|
122
|
-
end
|
117
|
+
RunLoop::Logging.log_debug(logger, "Simulator instruction set '#{simulator.instruction_set}' is compatible with #{lipo.info}")
|
123
118
|
true
|
124
119
|
else
|
125
|
-
|
126
|
-
puts "Xcode #{sim_control.xctools.xcode_version} detected; skipping simulator architecture check."
|
127
|
-
end
|
120
|
+
RunLoop::Logging.log_debug(logger, "Xcode #{sim_control.xctools.xcode_version} detected; skipping simulator architecture check.")
|
128
121
|
false
|
129
122
|
end
|
130
123
|
end
|
@@ -132,6 +125,7 @@ module RunLoop
|
|
132
125
|
def self.run_with_options(options)
|
133
126
|
before = Time.now
|
134
127
|
|
128
|
+
logger = options[:logger]
|
135
129
|
sim_control ||= options[:sim_control] || RunLoop::SimControl.new
|
136
130
|
xctools ||= options[:xctools] || sim_control.xctools
|
137
131
|
|
@@ -170,7 +164,9 @@ module RunLoop
|
|
170
164
|
uia_strategy = options[:uia_strategy]
|
171
165
|
if uia_strategy == :host
|
172
166
|
create_uia_pipe(repl_path)
|
173
|
-
RunLoop::HostCache.default.clear
|
167
|
+
RunLoop::HostCache.default.clear unless RunLoop::Environment.xtc?
|
168
|
+
else
|
169
|
+
FileUtils.touch repl_path
|
174
170
|
end
|
175
171
|
|
176
172
|
cal_script = File.join(SCRIPTS_PATH, 'calabash_script_uia.js')
|
@@ -189,9 +185,7 @@ module RunLoop
|
|
189
185
|
log_file ||= File.join(results_dir, 'run_loop.out')
|
190
186
|
|
191
187
|
after = Time.now
|
192
|
-
|
193
|
-
puts "Preparation took #{after-before} seconds"
|
194
|
-
end
|
188
|
+
RunLoop::Logging.log_debug(logger, "Preparation took #{after-before} seconds")
|
195
189
|
|
196
190
|
discovered_options =
|
197
191
|
{
|
@@ -215,7 +209,7 @@ module RunLoop
|
|
215
209
|
|
216
210
|
automation_template = automation_template(xctools)
|
217
211
|
|
218
|
-
log_header("Starting on #{device_target} App: #{bundle_dir_or_bundle_id}")
|
212
|
+
RunLoop::Logging.log_header(logger, "Starting on #{device_target} App: #{bundle_dir_or_bundle_id}")
|
219
213
|
|
220
214
|
pid = instruments.spawn(automation_template, merged_options, log_file)
|
221
215
|
|
@@ -241,22 +235,24 @@ module RunLoop
|
|
241
235
|
options[:validate_channel].call(run_loop, 0, uia_timeout)
|
242
236
|
else
|
243
237
|
cmd = "UIALogger.logMessage('Listening for run loop commands')"
|
244
|
-
|
245
|
-
|
238
|
+
begin
|
239
|
+
fifo_timeout = options[:fifo_timeout] || 30
|
240
|
+
RunLoop::Fifo.write(repl_path, "0:#{cmd}", timeout: fifo_timeout)
|
241
|
+
rescue RunLoop::Fifo::NoReaderConfiguredError,
|
242
|
+
RunLoop::Fifo::WriteTimedOut => e
|
243
|
+
RunLoop::Logging.log_debug(logger, "Error while writing to fifo. #{e}")
|
244
|
+
raise RunLoop::TimeoutError.new("Error while writing to fifo. #{e}")
|
245
|
+
end
|
246
|
+
Timeout::timeout(timeout, RunLoop::TimeoutError) do
|
246
247
|
read_response(run_loop, 0, uia_timeout)
|
247
248
|
end
|
248
249
|
end
|
249
|
-
rescue TimeoutError => e
|
250
|
-
|
251
|
-
|
252
|
-
puts "#{e}: #{e && e.message}"
|
253
|
-
end
|
254
|
-
raise TimeoutError, "Time out waiting for UIAutomation run-loop to Start. \n Logfile #{log_file} \n\n #{File.read(log_file)}\n"
|
250
|
+
rescue RunLoop::TimeoutError => e
|
251
|
+
RunLoop::Logging.log_debug(logger, "Failed to launch. #{e}: #{e && e.message}")
|
252
|
+
raise RunLoop::TimeoutError, "Time out waiting for UIAutomation run-loop #{e}. \n Logfile #{log_file} \n\n #{File.read(log_file)}\n"
|
255
253
|
end
|
256
254
|
|
257
|
-
|
258
|
-
puts "Launching took #{Time.now-before} seconds"
|
259
|
-
end
|
255
|
+
RunLoop::Logging.log_debug(logger, "Launching took #{Time.now-before} seconds")
|
260
256
|
|
261
257
|
dylib_path = self.dylib_path_from_options(merged_options)
|
262
258
|
|
@@ -413,7 +409,7 @@ module RunLoop
|
|
413
409
|
|
414
410
|
def self.create_uia_pipe(repl_path)
|
415
411
|
begin
|
416
|
-
Timeout::timeout(5, TimeoutError) do
|
412
|
+
Timeout::timeout(5, RunLoop::TimeoutError) do
|
417
413
|
loop do
|
418
414
|
begin
|
419
415
|
FileUtils.rm_f(repl_path)
|
@@ -424,8 +420,8 @@ module RunLoop
|
|
424
420
|
end
|
425
421
|
end
|
426
422
|
end
|
427
|
-
rescue TimeoutError => _
|
428
|
-
raise TimeoutError, 'Unable to create pipe (mkfifo failed)'
|
423
|
+
rescue RunLoop::TimeoutError => _
|
424
|
+
raise RunLoop::TimeoutError, 'Unable to create pipe (mkfifo failed)'
|
429
425
|
end
|
430
426
|
end
|
431
427
|
|
@@ -437,33 +433,37 @@ module RunLoop
|
|
437
433
|
repl_path = run_loop[:repl_path]
|
438
434
|
index = run_loop[:index]
|
439
435
|
cmd_str = "#{index}:#{escape_host_command(cmd)}"
|
440
|
-
|
441
|
-
RunLoop.log_info(logger, cmd_str) if should_log
|
436
|
+
RunLoop::Logging.log_debug(logger, cmd_str)
|
442
437
|
write_succeeded = false
|
443
438
|
2.times do |i|
|
444
|
-
RunLoop.
|
445
|
-
|
446
|
-
|
439
|
+
RunLoop::Logging.log_debug(logger, "Trying write of command #{cmd_str} at index #{index}")
|
440
|
+
begin
|
441
|
+
RunLoop::Fifo.write(repl_path, cmd_str)
|
442
|
+
write_succeeded = validate_index_written(run_loop, index, logger)
|
443
|
+
rescue RunLoop::Fifo::NoReaderConfiguredError,
|
444
|
+
RunLoop::Fifo::WriteTimedOut => e
|
445
|
+
RunLoop::Logging.log_debug(logger, "Error while writing command (retry count #{i}). #{e}")
|
446
|
+
end
|
447
447
|
break if write_succeeded
|
448
448
|
end
|
449
449
|
unless write_succeeded
|
450
|
-
RunLoop.
|
450
|
+
RunLoop::Logging.log_debug(logger, 'Failing...Raising RunLoop::WriteFailedError')
|
451
451
|
raise RunLoop::WriteFailedError.new("Trying write of command #{cmd_str} at index #{index}")
|
452
452
|
end
|
453
453
|
run_loop[:index] = index + 1
|
454
|
-
RunLoop::HostCache.default.write(run_loop)
|
454
|
+
RunLoop::HostCache.default.write(run_loop) unless RunLoop::Environment.xtc?
|
455
455
|
index
|
456
456
|
end
|
457
457
|
|
458
458
|
def self.validate_index_written(run_loop, index, logger)
|
459
459
|
begin
|
460
|
-
Timeout::timeout(10, TimeoutError) do
|
460
|
+
Timeout::timeout(10, RunLoop::TimeoutError) do
|
461
461
|
Core.read_response(run_loop, index, 10, 'last_index')
|
462
462
|
end
|
463
|
-
RunLoop.
|
463
|
+
RunLoop::Logging.log_debug(logger, "validate index written for index #{index} ok")
|
464
464
|
return true
|
465
|
-
rescue TimeoutError => _
|
466
|
-
RunLoop.
|
465
|
+
rescue RunLoop::TimeoutError => _
|
466
|
+
RunLoop::Logging.log_debug(logger, "validate index written for index #{index} failed. Retrying.")
|
467
467
|
return false
|
468
468
|
end
|
469
469
|
end
|
@@ -485,9 +485,7 @@ module RunLoop
|
|
485
485
|
next
|
486
486
|
end
|
487
487
|
|
488
|
-
|
489
488
|
size = File.size(log_file)
|
490
|
-
|
491
489
|
output = File.read(log_file, size-offset, offset)
|
492
490
|
|
493
491
|
if /AXError: Could not auto-register for pid status change/.match(output)
|
@@ -495,10 +493,10 @@ module RunLoop
|
|
495
493
|
$stderr.puts "\n\n****** Accessibility is not enabled on device/simulator, please enable it *** \n\n"
|
496
494
|
$stderr.flush
|
497
495
|
end
|
498
|
-
raise TimeoutError.new('AXError: Could not auto-register for pid status change')
|
496
|
+
raise RunLoop::TimeoutError.new('AXError: Could not auto-register for pid status change')
|
499
497
|
end
|
500
498
|
if /Automation Instrument ran into an exception/.match(output)
|
501
|
-
raise TimeoutError.new('Exception while running script')
|
499
|
+
raise RunLoop::TimeoutError.new('Exception while running script')
|
502
500
|
end
|
503
501
|
index_if_found = output.index(START_DELIMITER)
|
504
502
|
if ENV['DEBUG_READ']=='1'
|
@@ -545,7 +543,7 @@ module RunLoop
|
|
545
543
|
end
|
546
544
|
|
547
545
|
run_loop[:initial_offset] = offset
|
548
|
-
RunLoop::HostCache.default.write(run_loop)
|
546
|
+
RunLoop::HostCache.default.write(run_loop) unless RunLoop::Environment.xtc?
|
549
547
|
result
|
550
548
|
end
|
551
549
|
|
@@ -588,20 +586,6 @@ module RunLoop
|
|
588
586
|
raise msgs.join("\n")
|
589
587
|
end
|
590
588
|
|
591
|
-
def self.log(message)
|
592
|
-
if ENV['DEBUG']=='1'
|
593
|
-
puts "#{Time.now } #{message}"
|
594
|
-
$stdout.flush
|
595
|
-
end
|
596
|
-
end
|
597
|
-
|
598
|
-
def self.log_header(message)
|
599
|
-
if ENV['DEBUG']=='1'
|
600
|
-
puts "\n\e[#{35}m### #{message} ###\e[0m"
|
601
|
-
$stdout.flush
|
602
|
-
end
|
603
|
-
end
|
604
|
-
|
605
589
|
# @deprecated 1.0.5
|
606
590
|
def self.ensure_instruments_not_running!
|
607
591
|
RunLoop::Instruments.new.kill_instruments
|
@@ -617,170 +601,4 @@ module RunLoop
|
|
617
601
|
end
|
618
602
|
end
|
619
603
|
|
620
|
-
def self.default_script_for_uia_strategy(uia_strategy)
|
621
|
-
case uia_strategy
|
622
|
-
when :preferences
|
623
|
-
Core.script_for_key(:run_loop_fast_uia)
|
624
|
-
when :host
|
625
|
-
Core.script_for_key(:run_loop_host)
|
626
|
-
when :shared_element
|
627
|
-
Core.script_for_key(:run_loop_shared_element)
|
628
|
-
else
|
629
|
-
Core.script_for_key(:run_loop_basic)
|
630
|
-
end
|
631
|
-
end
|
632
|
-
|
633
|
-
def self.run(options={})
|
634
|
-
|
635
|
-
if RunLoop::Instruments.new.instruments_app_running?
|
636
|
-
msg =
|
637
|
-
[
|
638
|
-
"Please quit the Instruments.app.",
|
639
|
-
"If Instruments.app is open, the instruments command line",
|
640
|
-
"tool cannot take control of your application."
|
641
|
-
]
|
642
|
-
raise msg.join("\n")
|
643
|
-
end
|
644
|
-
|
645
|
-
uia_strategy = options[:uia_strategy]
|
646
|
-
if options[:script]
|
647
|
-
script = validate_script(options[:script])
|
648
|
-
else
|
649
|
-
if uia_strategy
|
650
|
-
script = default_script_for_uia_strategy(uia_strategy)
|
651
|
-
else
|
652
|
-
if options[:calabash_lite]
|
653
|
-
uia_strategy = :host
|
654
|
-
script = Core.script_for_key(:run_loop_host)
|
655
|
-
else
|
656
|
-
uia_strategy = :preferences
|
657
|
-
script = default_script_for_uia_strategy(uia_strategy)
|
658
|
-
end
|
659
|
-
end
|
660
|
-
end
|
661
|
-
# At this point, 'script' has been chosen, but uia_strategy might not
|
662
|
-
unless uia_strategy
|
663
|
-
desired_script = options[:script]
|
664
|
-
if desired_script.is_a?(String) #custom path to script
|
665
|
-
uia_strategy = :host
|
666
|
-
elsif desired_script == :run_loop_host
|
667
|
-
uia_strategy = :host
|
668
|
-
elsif desired_script == :run_loop_fast_uia
|
669
|
-
uia_strategy = :preferences
|
670
|
-
elsif desired_script == :run_loop_shared_element
|
671
|
-
uia_strategy = :shared_element
|
672
|
-
else
|
673
|
-
raise "Inconsistent state: desired script #{desired_script} has not uia_strategy"
|
674
|
-
end
|
675
|
-
end
|
676
|
-
# At this point script and uia_strategy selected
|
677
|
-
|
678
|
-
options[:script] = script
|
679
|
-
options[:uia_strategy] = uia_strategy
|
680
|
-
|
681
|
-
Core.run_with_options(options)
|
682
|
-
end
|
683
|
-
|
684
|
-
def self.send_command(run_loop, cmd, options={timeout: 60}, num_retries=0, last_error=nil)
|
685
|
-
if num_retries > 3
|
686
|
-
if last_error
|
687
|
-
raise last_error
|
688
|
-
else
|
689
|
-
raise "Max retries exceeded #{num_retries} > 3. No error recorded."
|
690
|
-
end
|
691
|
-
end
|
692
|
-
|
693
|
-
if options.is_a?(Numeric)
|
694
|
-
options = {timeout: options}
|
695
|
-
end
|
696
|
-
|
697
|
-
if not cmd.is_a?(String)
|
698
|
-
raise "Illegal command #{cmd} (must be a string)"
|
699
|
-
end
|
700
|
-
|
701
|
-
if not options.is_a?(Hash)
|
702
|
-
raise "Illegal options #{options} (must be a Hash (or number for compatibility))"
|
703
|
-
end
|
704
|
-
|
705
|
-
timeout = options[:timeout] || 60
|
706
|
-
logger = options[:logger]
|
707
|
-
interrupt_retry_timeout = options[:interrupt_retry_timeout] || 25
|
708
|
-
|
709
|
-
expected_index = run_loop[:index]
|
710
|
-
result = nil
|
711
|
-
begin
|
712
|
-
expected_index = Core.write_request(run_loop, cmd, logger)
|
713
|
-
rescue RunLoop::WriteFailedError, Errno::EINTR => write_error
|
714
|
-
# Attempt recover from interrupt by attempting to read result (assuming write went OK)
|
715
|
-
# or retry if attempted read result fails
|
716
|
-
run_loop[:index] = expected_index # restore expected index in case it changed
|
717
|
-
log_info(logger, "Core.write_request failed: #{write_error}. Attempting recovery...")
|
718
|
-
log_info(logger, "Attempting read in case the request was received... Please wait (#{interrupt_retry_timeout})...")
|
719
|
-
begin
|
720
|
-
Timeout::timeout(interrupt_retry_timeout, TimeoutError) do
|
721
|
-
result = Core.read_response(run_loop, expected_index)
|
722
|
-
end
|
723
|
-
# Update run_loop expected index since we succeeded in reading the index
|
724
|
-
run_loop[:index] = expected_index + 1
|
725
|
-
log_info(logger, "Did read response for interrupted request of index #{expected_index}... Proceeding.")
|
726
|
-
return result
|
727
|
-
rescue TimeoutError => _
|
728
|
-
log_info(logger, "Read did not result in a response for index #{expected_index}... Retrying send_command...")
|
729
|
-
return send_command(run_loop, cmd, options, num_retries+1, write_error)
|
730
|
-
end
|
731
|
-
end
|
732
|
-
|
733
|
-
|
734
|
-
begin
|
735
|
-
Timeout::timeout(timeout, TimeoutError) do
|
736
|
-
result = Core.read_response(run_loop, expected_index)
|
737
|
-
end
|
738
|
-
rescue TimeoutError => _
|
739
|
-
raise TimeoutError, "Time out waiting for UIAutomation run-loop for command #{cmd}. Waiting for index:#{expected_index}"
|
740
|
-
end
|
741
|
-
|
742
|
-
result
|
743
|
-
end
|
744
|
-
|
745
|
-
def self.stop(run_loop, out=Dir.pwd)
|
746
|
-
return if run_loop.nil?
|
747
|
-
results_dir = run_loop[:results_dir]
|
748
|
-
dest = out
|
749
|
-
|
750
|
-
RunLoop::Instruments.new.kill_instruments
|
751
|
-
|
752
|
-
FileUtils.mkdir_p(dest)
|
753
|
-
if results_dir
|
754
|
-
pngs = Dir.glob(File.join(results_dir, 'Run 1', '*.png'))
|
755
|
-
else
|
756
|
-
pngs = []
|
757
|
-
end
|
758
|
-
FileUtils.cp(pngs, dest) if pngs and pngs.length > 0
|
759
|
-
end
|
760
|
-
|
761
|
-
|
762
|
-
def self.validate_script(script)
|
763
|
-
if script.is_a?(String)
|
764
|
-
unless File.exist?(script)
|
765
|
-
raise "Unable to find file: #{script}"
|
766
|
-
end
|
767
|
-
elsif script.is_a?(Symbol)
|
768
|
-
script = Core.script_for_key(script)
|
769
|
-
unless script
|
770
|
-
raise "Unknown script for symbol: #{script}. Options: #{Core::SCRIPTS.keys.join(', ')}"
|
771
|
-
end
|
772
|
-
else
|
773
|
-
raise "Script must be a symbol or path: #{script}"
|
774
|
-
end
|
775
|
-
script
|
776
|
-
end
|
777
|
-
|
778
|
-
def self.log_info(logger, message)
|
779
|
-
msg = "#{Time.now}: #{message}"
|
780
|
-
if logger && logger.respond_to?(:info)
|
781
|
-
logger.info(msg)
|
782
|
-
else
|
783
|
-
puts msg if ENV['DEBUG'] == '1'
|
784
|
-
end
|
785
|
-
end
|
786
604
|
end
|
data/lib/run_loop/environment.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
module RunLoop
|
3
|
+
module Fifo
|
4
|
+
BUFFER_SIZE = 4096
|
5
|
+
|
6
|
+
class NoReaderConfiguredError < RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
class WriteTimedOut < RuntimeError
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.write(pipe, msg, options={})
|
13
|
+
msg = "#{msg}\n"
|
14
|
+
timeout = options[:timeout] || 10
|
15
|
+
begin_at = Time.now
|
16
|
+
begin
|
17
|
+
open(pipe, File::WRONLY | File::NONBLOCK) do |pipe_io|
|
18
|
+
bytes_written = 0
|
19
|
+
bytes_to_write = msg.length
|
20
|
+
until bytes_written >= bytes_to_write do
|
21
|
+
begin
|
22
|
+
wrote = pipe_io.write_nonblock msg
|
23
|
+
bytes_written += wrote
|
24
|
+
msg = msg[wrote..-1]
|
25
|
+
rescue IO::WaitWritable, Errno::EINTR
|
26
|
+
timeout_left = timeout - (Time.now - begin_at)
|
27
|
+
raise WriteTimedOut if timeout_left <= 0
|
28
|
+
IO.select nil, [pipe_io], nil, timeout_left
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue Errno::ENXIO
|
33
|
+
sleep(0.5)
|
34
|
+
timeout_left = timeout - (Time.now - begin_at)
|
35
|
+
raise NoReaderConfiguredError if timeout_left <= 0
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/run_loop/instruments.rb
CHANGED
@@ -70,10 +70,8 @@ module RunLoop
|
|
70
70
|
# @todo Is this jruby compatible?
|
71
71
|
def spawn(automation_template, options, log_file)
|
72
72
|
splat_args = spawn_arguments(automation_template, options)
|
73
|
-
|
74
|
-
|
75
|
-
$stdout.flush
|
76
|
-
end
|
73
|
+
logger = options[:logger]
|
74
|
+
RunLoop::Logging.log_debug(logger, "xcrun #{splat_args.join(' ')} >& #{log_file}")
|
77
75
|
pid = Process.spawn('xcrun', *splat_args, {:out => log_file, :err => log_file})
|
78
76
|
Process.detach(pid)
|
79
77
|
pid.to_i
|
data/lib/run_loop/lldb.rb
CHANGED
@@ -34,9 +34,7 @@ module RunLoop
|
|
34
34
|
def self.kill_lldb_processes
|
35
35
|
self.lldb_pids.each do |pid|
|
36
36
|
unless self.kill_with_signal(pid, 'TERM')
|
37
|
-
|
38
|
-
self.kill_with_signal(pid, 'KILL')
|
39
|
-
end
|
37
|
+
self.kill_with_signal(pid, 'KILL')
|
40
38
|
end
|
41
39
|
end
|
42
40
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RunLoop
|
2
|
+
class Logging
|
3
|
+
|
4
|
+
def self.log_info(logger, message)
|
5
|
+
log_level :info, logger, message
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.log_debug(logger, message)
|
9
|
+
log_level :debug, logger, message
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.log_header(logger, message)
|
13
|
+
msg = "\n\e[#{35}m### #{message} ###\e[0m"
|
14
|
+
if logger.respond_to?(:debug)
|
15
|
+
logger.debug(msg)
|
16
|
+
else
|
17
|
+
debug_puts(msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.log_level(level, logger, message)
|
22
|
+
level = level.to_sym
|
23
|
+
msg = "#{Time.now} [RunLoop:#{level}]: #{message}"
|
24
|
+
if logger.respond_to?(level)
|
25
|
+
logger.send(level, msg)
|
26
|
+
else
|
27
|
+
debug_puts(msg)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.debug_puts(msg)
|
32
|
+
puts msg if RunLoop::Environment.debug?
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -23,6 +23,44 @@ module RunLoop
|
|
23
23
|
!pids.empty?
|
24
24
|
end
|
25
25
|
|
26
|
+
# Wait for a number of process to start.
|
27
|
+
# @param [Integer] n The number of processes to wait for.
|
28
|
+
# @raise [ArgumentError] If n < 0
|
29
|
+
# @raise [ArgumentError] If n is not an Integer
|
30
|
+
def wait_for_n(n)
|
31
|
+
unless n.is_a?(Integer)
|
32
|
+
raise ArgumentError, "Expected #{n.class} to be #{1.class}"
|
33
|
+
end
|
34
|
+
|
35
|
+
unless n > 0
|
36
|
+
raise ArgumentError, "Expected #{n} to be > 0"
|
37
|
+
end
|
38
|
+
|
39
|
+
return true if pids.count == n
|
40
|
+
|
41
|
+
now = Time.now
|
42
|
+
poll_until = now + @options[:timeout]
|
43
|
+
delay = @options[:interval]
|
44
|
+
there_are_n = false
|
45
|
+
while Time.now < poll_until
|
46
|
+
there_are_n = pids.count == n
|
47
|
+
break if there_are_n
|
48
|
+
sleep delay
|
49
|
+
end
|
50
|
+
|
51
|
+
if RunLoop::Environment.debug?
|
52
|
+
plural = n > 1 ? "es" : ''
|
53
|
+
puts "Waited for #{Time.now - now} seconds for #{n} '#{process_name}' process#{plural} to start."
|
54
|
+
end
|
55
|
+
|
56
|
+
if @options[:raise_on_timeout] and !there_are_n
|
57
|
+
plural = n > 1 ? "es" : ''
|
58
|
+
raise "Waited #{@options[:timeout]} seconds for #{n} '#{process_name}' process#{plural} to start."
|
59
|
+
end
|
60
|
+
there_are_n
|
61
|
+
end
|
62
|
+
|
63
|
+
|
26
64
|
# Wait for `process_name` to start.
|
27
65
|
def wait_for_any
|
28
66
|
return true if running_process?
|
data/lib/run_loop/sim_control.rb
CHANGED
@@ -762,6 +762,7 @@ module RunLoop
|
|
762
762
|
# updated or if the directory was skipped (see code comments).
|
763
763
|
# @raise [RuntimeError] If called when Xcode 6 is _not_ the active Xcode version.
|
764
764
|
def enable_keyboard_in_sim_data_dir(sim_data_dir, sim_details_keyed_with_udid, opts={})
|
765
|
+
|
765
766
|
unless xcode_version_gte_6?
|
766
767
|
raise RuntimeError, 'it is illegal to call this method when the Xcode < 6 is the current Xcode version'
|
767
768
|
end
|
data/lib/run_loop/version.rb
CHANGED
@@ -0,0 +1,7 @@
|
|
1
|
+
(function(){function m(){return h.frontMostApp()}function r(){return m().mainWindow()}function s(){return m().windows().toArray()}function n(){return m().keyboard()}function l(a,c){this.reason=a;this.a=c||"";this.message=this.toString()}function k(a){return!a||a instanceof UIAElementNil}function t(a,c){var b=c||[],e,d;if(k(a))return b;e=a.elements();for(var f=0,g=e.length;f<g;f+=1)d=e[f],b.push(d),t(d,b);return b}function q(a,c){for(var b=0,e=c.length;b<e;b+=1)a.push(c[b])}function u(a,c){var b=[];
|
2
|
+
if(k(c))return b;c instanceof this[a]&&b.push(c);for(var e=c.elements(),d=0,f=e.length;d<f;d+=1)q(b,u(a,e[d]));return b}function x(a,c){var b=null;if(a instanceof Array){if(3===a.length){var e=a[0],b=a[1],d=a[2];if("string"==typeof d)if(-1==d.indexOf("'"))d="'"+d+"'";else if(-1==d.indexOf('"'))d='"'+d+'"';else throw new l("Escaping for filters not supported yet.");b=c.withPredicate(e+" "+b+" "+d);return!k(b)}return!1}for(e in a)if(a.hasOwnProperty(e))if(b=a[e],"marked"==e){if(c.name()!=b&&c.label()!=
|
3
|
+
b&&(!c.value||c.value!=b))return!1}else if(b=c.withValueForKey(b,e),k(b))return!1;return!0}function v(a,c){if(c(a))return a;var b,e;if(k(a))return null;b=a.elements();for(var d=0,f=b.length;d<f;d+=1)if(e=b[d],v(e,c))return e;return null}function w(a){h.delay(a);return h}var g={},h=UIATarget.localTarget();h.setTimeout(0);l.prototype=error();l.prototype.toString=function(){var a="UIAutomationError[reason="+this.reason;0<this.a.length&&(a+=", details="+this.a);return a+"]"};g.sleep=w;g.query=function(a,
|
4
|
+
c){if(!c)return g.query(a,s());c instanceof UIAElement&&(c=[c]);var b=c,e=null,d=null,f=[],p,h,k,l;p=0;for(k=a.length;p<k;p+=1){e=a[p];h=0;for(l=b.length;h<l;h+=1)d=b[h],"string"===typeof e?"*"===e||"view"==e||"UIAElement"===e?q(f,t(d,[d])):q(f,u(e,d)):x(e,d)&&f.push(d);b=f;f=[]}return b};g.keyboard_visible=function(){return!k(n())};g.keyboard_enter_text=function(a,c){if(!g.keyboard_visible())throw new l("Keyboard not visible");c=c||{};if(c.unsafe)return n().typeString(a),!0;var b=v(r(),function(a){return 1==
|
5
|
+
a.hasKeyboardFocus()});if(k(b))return n().typeString(a),!0;var e=c.initial_text||"",d=new Date,f=c.timeout||60,h=n();do try{return h.typeString(a),!0}catch(m){UIALogger.logMessage("keyboard_enter_text failed: "+m),UIALogger.logMessage("keyboard_enter_text retrying with restore to: "+e),b.setValue(e)}while(!(new Date-d>=1E3*f));throw new l("Unable to enter text","text: "+a+" failed after retrying for "+f);};g.deactivate=function(a){h.deactivateAppForDuration(a)};g.tap_offset=function(a,c,b){b=b||{};
|
6
|
+
return b.unsafe?function(){return c.apply(this,arguments)}:function(){var e=new Date,d=b.timeout||60,f=b.frequency||.1;do try{return c.apply(this,arguments)}catch(g){UIALogger.logMessage(a+"Error: "+g+". Arguments: "+arguments[0]+", "+arguments[1]),w(f)}while(!(new Date-e>=1E3*d));throw new l(a,"Arguments: "+arguments[0]+", "+arguments[1]);}}("tap_offset failed",function(a,c){h.tapWithOptions(a,c||{})},{timeout:60,frequency:.5});this.target=h;this.uia=g;g.app=m;g.window=r;g.windows=s;g.keyboard=n;
|
7
|
+
g.alert=function(){return m().alert()}})();
|
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.2.
|
4
|
+
version: 1.2.8
|
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-
|
11
|
+
date: 2015-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -241,10 +241,12 @@ files:
|
|
241
241
|
- lib/run_loop/device.rb
|
242
242
|
- lib/run_loop/dylib_injector.rb
|
243
243
|
- lib/run_loop/environment.rb
|
244
|
+
- lib/run_loop/fifo.rb
|
244
245
|
- lib/run_loop/host_cache.rb
|
245
246
|
- lib/run_loop/instruments.rb
|
246
247
|
- lib/run_loop/lipo.rb
|
247
248
|
- lib/run_loop/lldb.rb
|
249
|
+
- lib/run_loop/logging.rb
|
248
250
|
- lib/run_loop/monkey_patch.rb
|
249
251
|
- lib/run_loop/plist_buddy.rb
|
250
252
|
- lib/run_loop/process_terminator.rb
|
@@ -253,6 +255,7 @@ files:
|
|
253
255
|
- lib/run_loop/simctl/bridge.rb
|
254
256
|
- lib/run_loop/version.rb
|
255
257
|
- lib/run_loop/xctools.rb
|
258
|
+
- scripts/calabash-uia-min.js
|
256
259
|
- scripts/calabash.lldb.erb
|
257
260
|
- scripts/calabash_script_uia.js
|
258
261
|
- scripts/json2-min.js
|
@@ -285,7 +288,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
285
288
|
version: '0'
|
286
289
|
requirements: []
|
287
290
|
rubyforge_project:
|
288
|
-
rubygems_version: 2.
|
291
|
+
rubygems_version: 2.2.2
|
289
292
|
signing_key:
|
290
293
|
specification_version: 4
|
291
294
|
summary: Tools related to running Calabash iOS tests
|