run_loop 1.2.7 → 1.2.8
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.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
|