run_loop_tcc 2.1.3
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 +7 -0
- data/LICENSE +21 -0
- data/bin/run-loop +19 -0
- data/lib/run_loop/abstract.rb +18 -0
- data/lib/run_loop/app.rb +372 -0
- data/lib/run_loop/cache/cache.rb +68 -0
- data/lib/run_loop/cli/cli.rb +48 -0
- data/lib/run_loop/cli/codesign.rb +24 -0
- data/lib/run_loop/cli/errors.rb +11 -0
- data/lib/run_loop/cli/instruments.rb +160 -0
- data/lib/run_loop/cli/locale.rb +31 -0
- data/lib/run_loop/cli/simctl.rb +257 -0
- data/lib/run_loop/cli/tcc.rb +139 -0
- data/lib/run_loop/codesign.rb +76 -0
- data/lib/run_loop/core.rb +902 -0
- data/lib/run_loop/core_simulator.rb +960 -0
- data/lib/run_loop/detect_aut/detect.rb +185 -0
- data/lib/run_loop/detect_aut/errors.rb +126 -0
- data/lib/run_loop/detect_aut/xamarin_studio.rb +46 -0
- data/lib/run_loop/detect_aut/xcode.rb +157 -0
- data/lib/run_loop/device.rb +722 -0
- data/lib/run_loop/device_agent/app/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/bin/xctestctl +0 -0
- data/lib/run_loop/device_agent/cbxrunner.rb +156 -0
- data/lib/run_loop/device_agent/frameworks/Frameworks.zip +0 -0
- data/lib/run_loop/device_agent/frameworks.rb +65 -0
- data/lib/run_loop/device_agent/ipa/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/launcher.rb +51 -0
- data/lib/run_loop/device_agent/xcodebuild.rb +91 -0
- data/lib/run_loop/device_agent/xctestctl.rb +109 -0
- data/lib/run_loop/directory.rb +179 -0
- data/lib/run_loop/dnssd.rb +148 -0
- data/lib/run_loop/dot_dir.rb +87 -0
- data/lib/run_loop/dylib_injector.rb +145 -0
- data/lib/run_loop/encoding.rb +56 -0
- data/lib/run_loop/environment.rb +361 -0
- data/lib/run_loop/fifo.rb +40 -0
- data/lib/run_loop/host_cache.rb +128 -0
- data/lib/run_loop/http/error.rb +15 -0
- data/lib/run_loop/http/request.rb +44 -0
- data/lib/run_loop/http/retriable_client.rb +166 -0
- data/lib/run_loop/http/server.rb +17 -0
- data/lib/run_loop/instruments.rb +436 -0
- data/lib/run_loop/ipa.rb +142 -0
- data/lib/run_loop/l10n.rb +93 -0
- data/lib/run_loop/language.rb +63 -0
- data/lib/run_loop/lipo.rb +132 -0
- data/lib/run_loop/lldb.rb +52 -0
- data/lib/run_loop/locale.rb +101 -0
- data/lib/run_loop/logging.rb +111 -0
- data/lib/run_loop/otool.rb +76 -0
- data/lib/run_loop/patches/awesome_print.rb +17 -0
- data/lib/run_loop/physical_device/life_cycle.rb +268 -0
- data/lib/run_loop/plist_buddy.rb +189 -0
- data/lib/run_loop/process_terminator.rb +128 -0
- data/lib/run_loop/process_waiter.rb +117 -0
- data/lib/run_loop/regex.rb +19 -0
- data/lib/run_loop/shell.rb +103 -0
- data/lib/run_loop/sim_control.rb +1264 -0
- data/lib/run_loop/simctl.rb +275 -0
- data/lib/run_loop/sqlite.rb +61 -0
- data/lib/run_loop/strings.rb +88 -0
- data/lib/run_loop/tcc/TCC.db +0 -0
- data/lib/run_loop/tcc/tcc.rb +240 -0
- data/lib/run_loop/template.rb +61 -0
- data/lib/run_loop/version.rb +182 -0
- data/lib/run_loop/xcode.rb +318 -0
- data/lib/run_loop/xcrun.rb +107 -0
- data/lib/run_loop/xcuitest.rb +550 -0
- data/lib/run_loop.rb +230 -0
- data/plists/simctl/com.apple.UIAutomation.plist +0 -0
- data/plists/simctl/com.apple.UIAutomationPlugIn.plist +0 -0
- data/scripts/calabash_script_uia.js +28184 -0
- data/scripts/lib/json2.min.js +26 -0
- data/scripts/lib/log.js +26 -0
- data/scripts/lib/on_alert.js +224 -0
- data/scripts/read-cmd.sh +2 -0
- data/scripts/run_dismiss_location.js +89 -0
- data/scripts/run_loop_basic.js +34 -0
- data/scripts/run_loop_fast_uia.js +188 -0
- data/scripts/run_loop_host.js +117 -0
- data/scripts/run_loop_shared_element.js +125 -0
- data/scripts/timeout3 +23 -0
- data/scripts/udidetect +0 -0
- data/vendor-licenses/FBSimulatorControl.LICENSE +30 -0
- data/vendor-licenses/xctestctl.LICENSE +32 -0
- metadata +443 -0
@@ -0,0 +1,902 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'timeout'
|
4
|
+
require 'json'
|
5
|
+
require 'open3'
|
6
|
+
require 'erb'
|
7
|
+
require 'ap'
|
8
|
+
|
9
|
+
module RunLoop
|
10
|
+
|
11
|
+
module Core
|
12
|
+
|
13
|
+
include RunLoop::Regex
|
14
|
+
|
15
|
+
START_DELIMITER = "OUTPUT_JSON:\n"
|
16
|
+
END_DELIMITER="\nEND_OUTPUT"
|
17
|
+
|
18
|
+
SCRIPTS = {
|
19
|
+
:dismiss => 'run_dismiss_location.js',
|
20
|
+
:run_loop_host => 'run_loop_host.js',
|
21
|
+
:run_loop_fast_uia => 'run_loop_fast_uia.js',
|
22
|
+
:run_loop_shared_element => 'run_loop_shared_element.js',
|
23
|
+
:run_loop_basic => 'run_loop_basic.js'
|
24
|
+
}
|
25
|
+
|
26
|
+
SCRIPTS_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'scripts'))
|
27
|
+
READ_SCRIPT_PATH = File.join(SCRIPTS_PATH, 'read-cmd.sh')
|
28
|
+
TIMEOUT_SCRIPT_PATH = File.join(SCRIPTS_PATH, 'timeout3')
|
29
|
+
|
30
|
+
def self.log_run_loop_options(options, xcode)
|
31
|
+
return unless RunLoop::Environment.debug?
|
32
|
+
# Ignore :sim_control b/c it is a ruby object; printing is not useful.
|
33
|
+
ignored_keys = [:sim_control]
|
34
|
+
options_to_log = {}
|
35
|
+
options.each_pair do |key, value|
|
36
|
+
next if ignored_keys.include?(key)
|
37
|
+
options_to_log[key] = value
|
38
|
+
end
|
39
|
+
# Objects that override '==' cannot be printed by awesome_print
|
40
|
+
# https://github.com/michaeldv/awesome_print/issues/154
|
41
|
+
# RunLoop::Version overrides '=='
|
42
|
+
options_to_log[:xcode] = xcode.version.to_s
|
43
|
+
options_to_log[:xcode_path] = xcode.developer_dir
|
44
|
+
message = options_to_log.ai({:sort_keys => true})
|
45
|
+
logger = options[:logger]
|
46
|
+
RunLoop::Logging.log_debug(logger, "\n" + message)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.script_for_key(key)
|
50
|
+
if SCRIPTS[key].nil?
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
SCRIPTS[key]
|
54
|
+
end
|
55
|
+
|
56
|
+
# @!visibility private
|
57
|
+
# This is the entry point for UIAutomation.
|
58
|
+
def self.run_with_options(options)
|
59
|
+
before = Time.now
|
60
|
+
|
61
|
+
self.prepare(options)
|
62
|
+
|
63
|
+
logger = options[:logger]
|
64
|
+
simctl = options[:sim_control] || options[:simctl] || RunLoop::Simctl.new
|
65
|
+
xcode = options[:xcode] || RunLoop::Xcode.new
|
66
|
+
instruments = options[:instruments] || RunLoop::Instruments.new
|
67
|
+
|
68
|
+
if xcode.version_gte_8?
|
69
|
+
raise %Q[
|
70
|
+
UIAutomation is not available on Xcode >= 8.*.
|
71
|
+
|
72
|
+
We are in the process of updating Calabash to use our new tool: DeviceAgent.
|
73
|
+
|
74
|
+
We will track progress in this forum post:
|
75
|
+
|
76
|
+
https://groups.google.com/forum/#!topic/calabash-ios/g34znf0LnE4
|
77
|
+
|
78
|
+
For now, testing with Xcode 8 is not supported.
|
79
|
+
|
80
|
+
Thank you for your patience.
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Device under test: DUT
|
85
|
+
device = RunLoop::Device.detect_device(options, xcode, simctl, instruments)
|
86
|
+
|
87
|
+
# App under test: AUT
|
88
|
+
app_details = RunLoop::DetectAUT.detect_app_under_test(options)
|
89
|
+
|
90
|
+
# Find the script to pass to instruments and the strategy to communicate
|
91
|
+
# with UIAutomation.
|
92
|
+
script_n_strategy = self.detect_instruments_script_and_strategy(options,
|
93
|
+
device,
|
94
|
+
xcode)
|
95
|
+
instruments_script = script_n_strategy[:script]
|
96
|
+
uia_strategy = script_n_strategy[:strategy]
|
97
|
+
|
98
|
+
# The app life cycle reset options.
|
99
|
+
reset_options = self.detect_reset_options(options)
|
100
|
+
|
101
|
+
instruments.kill_instruments(xcode)
|
102
|
+
|
103
|
+
timeout = options[:timeout] || 30
|
104
|
+
|
105
|
+
results_dir = options[:results_dir] || RunLoop::DotDir.make_results_dir
|
106
|
+
results_dir_trace = File.join(results_dir, 'trace')
|
107
|
+
FileUtils.mkdir_p(results_dir_trace)
|
108
|
+
|
109
|
+
dependencies = options[:dependencies] || []
|
110
|
+
dependencies << File.join(SCRIPTS_PATH, 'calabash_script_uia.js')
|
111
|
+
dependencies.each do |dep|
|
112
|
+
FileUtils.cp(dep, results_dir)
|
113
|
+
end
|
114
|
+
|
115
|
+
script = File.join(results_dir, '_run_loop.js')
|
116
|
+
|
117
|
+
javascript = UIAScriptTemplate.new(SCRIPTS_PATH, instruments_script).result
|
118
|
+
UIAScriptTemplate.sub_path_var!(javascript, results_dir)
|
119
|
+
UIAScriptTemplate.sub_read_script_path_var!(javascript, READ_SCRIPT_PATH)
|
120
|
+
UIAScriptTemplate.sub_timeout_script_path_var!(javascript, TIMEOUT_SCRIPT_PATH)
|
121
|
+
|
122
|
+
# Using a :no_* option is confusing.
|
123
|
+
# TODO Replace :no_flush with :flush_uia_logs; it should default to true
|
124
|
+
if RunLoop::Environment.xtc?
|
125
|
+
UIAScriptTemplate.sub_mode_var!(javascript, "FLUSH") unless options[:no_flush]
|
126
|
+
else
|
127
|
+
if self.detect_flush_uia_log_option(options)
|
128
|
+
UIAScriptTemplate.sub_flush_uia_logs_var!(javascript, "FLUSH_LOGS")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
repl_path = File.join(results_dir, 'repl-cmd.pipe')
|
133
|
+
FileUtils.rm_f(repl_path)
|
134
|
+
|
135
|
+
if uia_strategy == :host
|
136
|
+
create_uia_pipe(repl_path)
|
137
|
+
else
|
138
|
+
FileUtils.touch repl_path
|
139
|
+
end
|
140
|
+
|
141
|
+
RunLoop::HostCache.default.clear unless RunLoop::Environment.xtc?
|
142
|
+
|
143
|
+
cal_script = File.join(SCRIPTS_PATH, 'calabash_script_uia.js')
|
144
|
+
File.open(script, 'w') do |file|
|
145
|
+
if include_calabash_script?(options)
|
146
|
+
file.puts IO.read(cal_script)
|
147
|
+
end
|
148
|
+
file.puts javascript
|
149
|
+
end
|
150
|
+
|
151
|
+
args = options.fetch(:args, [])
|
152
|
+
|
153
|
+
log_file = options[:log_path] || File.join(results_dir, 'run_loop.out')
|
154
|
+
|
155
|
+
discovered_options =
|
156
|
+
{
|
157
|
+
:udid => device.udid,
|
158
|
+
:device => device,
|
159
|
+
:results_dir_trace => results_dir_trace,
|
160
|
+
:bundle_id => app_details[:bundle_id],
|
161
|
+
:app => app_details[:app] || app_details[:bundle_id],
|
162
|
+
:results_dir => results_dir,
|
163
|
+
:script => script,
|
164
|
+
:log_file => log_file,
|
165
|
+
:args => args,
|
166
|
+
:uia_strategy => uia_strategy,
|
167
|
+
:base_script => instruments_script
|
168
|
+
}
|
169
|
+
merged_options = options.merge(discovered_options)
|
170
|
+
|
171
|
+
if device.simulator?
|
172
|
+
if !app_details[:app]
|
173
|
+
raise %Q[
|
174
|
+
|
175
|
+
Invalid APP, APP_BUNDLE_PATH, or BUNDLE_ID detected.
|
176
|
+
|
177
|
+
The following information was detected from the environment:
|
178
|
+
|
179
|
+
APP='#{ENV["APP"]}'
|
180
|
+
APP_BUNDLE_PATH='#{ENV["APP_BUNDLE_PATH"]}'
|
181
|
+
BUNDLE_ID='#{ENV["BUNDLE_ID"]}'
|
182
|
+
|
183
|
+
|
184
|
+
It looks like you are trying to launch an app on a simulator using a bundle
|
185
|
+
identifier or you have incorrectly set the APP variable to an app bundle that
|
186
|
+
does not exist.
|
187
|
+
|
188
|
+
If you are trying to launch a test against a physical device, set the DEVICE_TARGET
|
189
|
+
variable to the UDID of your device an APP to a bundle identifier or a path to
|
190
|
+
an .ipa.
|
191
|
+
|
192
|
+
# com.example.MyApp must be installed on the target device
|
193
|
+
$ APP=com.example.MyApp DEVICE_TARGET="John's iPhone" cucumber
|
194
|
+
|
195
|
+
If you are trying to launch against a simulator and you encounter this error, it
|
196
|
+
means that the APP variable is pointing to a .app that does not exist.
|
197
|
+
|
198
|
+
]
|
199
|
+
end
|
200
|
+
self.prepare_simulator(app_details[:app], device, xcode, simctl, reset_options)
|
201
|
+
end
|
202
|
+
|
203
|
+
self.log_run_loop_options(merged_options, xcode)
|
204
|
+
|
205
|
+
automation_template = automation_template(instruments)
|
206
|
+
|
207
|
+
RunLoop::Logging.log_header(logger, "Starting on #{device.name} App: #{app_details[:bundle_id]}")
|
208
|
+
|
209
|
+
pid = instruments.spawn(automation_template, merged_options, log_file)
|
210
|
+
|
211
|
+
File.open(File.join(results_dir, 'run_loop.pid'), 'w') do |f|
|
212
|
+
f.write pid
|
213
|
+
end
|
214
|
+
|
215
|
+
run_loop = {
|
216
|
+
:pid => pid,
|
217
|
+
:index => 1,
|
218
|
+
:uia_strategy => uia_strategy,
|
219
|
+
:udid => device.udid,
|
220
|
+
:app => app_details[:bundle_id],
|
221
|
+
:repl_path => repl_path,
|
222
|
+
:log_file => log_file,
|
223
|
+
:results_dir => results_dir
|
224
|
+
}
|
225
|
+
|
226
|
+
uia_timeout = options[:uia_timeout] || RunLoop::Environment.uia_timeout || 10
|
227
|
+
|
228
|
+
RunLoop::Logging.log_debug(logger, "Preparation took #{Time.now-before} seconds")
|
229
|
+
|
230
|
+
before_instruments_launch = Time.now
|
231
|
+
|
232
|
+
fifo_retry_on = [
|
233
|
+
RunLoop::Fifo::NoReaderConfiguredError,
|
234
|
+
RunLoop::Fifo::WriteTimedOut
|
235
|
+
]
|
236
|
+
|
237
|
+
begin
|
238
|
+
|
239
|
+
if options[:validate_channel]
|
240
|
+
options[:validate_channel].call(run_loop, 0, uia_timeout)
|
241
|
+
else
|
242
|
+
|
243
|
+
cmd = "UIALogger.logMessage('Listening for run loop commands')"
|
244
|
+
|
245
|
+
begin
|
246
|
+
|
247
|
+
fifo_timeout = options[:fifo_timeout] || 30
|
248
|
+
RunLoop::Fifo.write(repl_path, "0:#{cmd}", timeout: fifo_timeout)
|
249
|
+
|
250
|
+
rescue *fifo_retry_on => e
|
251
|
+
|
252
|
+
message = "Error while writing to fifo. #{e}"
|
253
|
+
RunLoop::Logging.log_debug(logger, message)
|
254
|
+
raise RunLoop::TimeoutError.new(message)
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
Timeout::timeout(timeout, RunLoop::TimeoutError) do
|
259
|
+
read_response(run_loop, 0, uia_timeout)
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
rescue RunLoop::TimeoutError => e
|
264
|
+
RunLoop::Logging.log_debug(logger, "Failed to launch. #{e}: #{e && e.message}")
|
265
|
+
|
266
|
+
message = %Q(
|
267
|
+
|
268
|
+
"Timed out waiting for UIAutomation run-loop #{e}.
|
269
|
+
|
270
|
+
Logfile: #{log_file}
|
271
|
+
|
272
|
+
#{File.read(log_file)}
|
273
|
+
|
274
|
+
)
|
275
|
+
raise RunLoop::TimeoutError, message
|
276
|
+
end
|
277
|
+
|
278
|
+
RunLoop::Logging.log_debug(logger, "Launching took #{Time.now-before_instruments_launch} seconds")
|
279
|
+
|
280
|
+
dylib_path = self.dylib_path_from_options(merged_options)
|
281
|
+
|
282
|
+
if dylib_path
|
283
|
+
if device.physical_device?
|
284
|
+
raise RuntimeError, "Injecting a dylib is not supported when targeting a device"
|
285
|
+
end
|
286
|
+
|
287
|
+
app = app_details[:app]
|
288
|
+
lldb = RunLoop::DylibInjector.new(app.executable_name, dylib_path)
|
289
|
+
lldb.retriable_inject_dylib
|
290
|
+
end
|
291
|
+
|
292
|
+
RunLoop.log_debug("It took #{Time.now - before} seconds to launch the app")
|
293
|
+
run_loop
|
294
|
+
end
|
295
|
+
|
296
|
+
# @!visibility private
|
297
|
+
# Usually we include CalabashScript to ease uia automation.
|
298
|
+
# However in certain scenarios we don't load it since
|
299
|
+
# it slows down the UIAutomation initialization process
|
300
|
+
# occasionally causing privacy/security dialogs not to be automated.
|
301
|
+
#
|
302
|
+
# @return {boolean} true if CalabashScript should be loaded
|
303
|
+
def self.include_calabash_script?(options)
|
304
|
+
|
305
|
+
if (options[:include_calabash_script] == false) || options[:dismiss_immediate_dialogs]
|
306
|
+
return false
|
307
|
+
end
|
308
|
+
if Core.script_for_key(:run_loop_basic) == options[:script]
|
309
|
+
return options[:include_calabash_script]
|
310
|
+
end
|
311
|
+
true
|
312
|
+
end
|
313
|
+
|
314
|
+
# Extracts the value of :inject_dylib from options Hash.
|
315
|
+
# @param options [Hash] arguments passed to {RunLoop.run}
|
316
|
+
# @return [String, nil] If the options contains :inject_dylibs and it is a
|
317
|
+
# path to a dylib that exists, return the path. Otherwise return nil or
|
318
|
+
# raise an error.
|
319
|
+
# @raise [RuntimeError] If :inject_dylib points to a path that does not exist.
|
320
|
+
# @raise [ArgumentError] If :inject_dylib is not a String.
|
321
|
+
def self.dylib_path_from_options(options)
|
322
|
+
inject_dylib = options.fetch(:inject_dylib, nil)
|
323
|
+
return nil if inject_dylib.nil?
|
324
|
+
unless inject_dylib.is_a? String
|
325
|
+
raise ArgumentError, "Expected :inject_dylib to be a path to a dylib, but found '#{inject_dylib}'"
|
326
|
+
end
|
327
|
+
dylib_path = File.expand_path(inject_dylib)
|
328
|
+
unless File.exist?(dylib_path)
|
329
|
+
raise "Cannot load dylib. The file '#{dylib_path}' does not exist."
|
330
|
+
end
|
331
|
+
dylib_path
|
332
|
+
end
|
333
|
+
|
334
|
+
# Returns the a default simulator to target. This default needs to be one
|
335
|
+
# that installed by default in the current Xcode version.
|
336
|
+
#
|
337
|
+
# For historical reasons, the most recent non-64b SDK should be used.
|
338
|
+
#
|
339
|
+
# @param [RunLoop::Xcode] xcode Used to detect the current xcode
|
340
|
+
# version.
|
341
|
+
def self.default_simulator(xcode=RunLoop::Xcode.new)
|
342
|
+
|
343
|
+
if xcode.version_gte_8?
|
344
|
+
"iPhone 6s (10.0)"
|
345
|
+
elsif xcode.version_gte_73?
|
346
|
+
"iPhone 6s (9.3)"
|
347
|
+
elsif xcode.version_gte_72?
|
348
|
+
"iPhone 6s (9.2)"
|
349
|
+
elsif xcode.version_gte_71?
|
350
|
+
"iPhone 6s (9.1)"
|
351
|
+
elsif xcode.version_gte_7?
|
352
|
+
"iPhone 5s (9.0)"
|
353
|
+
elsif xcode.version_gte_64?
|
354
|
+
"iPhone 5s (8.4 Simulator)"
|
355
|
+
elsif xcode.version_gte_63?
|
356
|
+
"iPhone 5s (8.3 Simulator)"
|
357
|
+
elsif xcode.version_gte_62?
|
358
|
+
"iPhone 5s (8.2 Simulator)"
|
359
|
+
elsif xcode.version_gte_61?
|
360
|
+
"iPhone 5s (8.1 Simulator)"
|
361
|
+
else
|
362
|
+
"iPhone 5s (8.0 Simulator)"
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
def self.create_uia_pipe(repl_path)
|
368
|
+
begin
|
369
|
+
Timeout::timeout(5, RunLoop::TimeoutError) do
|
370
|
+
loop do
|
371
|
+
begin
|
372
|
+
FileUtils.rm_f(repl_path)
|
373
|
+
return repl_path if system(%Q[mkfifo "#{repl_path}"])
|
374
|
+
rescue Errno::EINTR => _
|
375
|
+
sleep(0.1)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
rescue RunLoop::TimeoutError => _
|
380
|
+
raise RunLoop::TimeoutError, 'Unable to create pipe (mkfifo failed)'
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def self.jruby?
|
385
|
+
RUBY_PLATFORM == 'java'
|
386
|
+
end
|
387
|
+
|
388
|
+
def self.write_request(run_loop, cmd, logger=nil)
|
389
|
+
repl_path = run_loop[:repl_path]
|
390
|
+
index = run_loop[:index]
|
391
|
+
cmd_str = "#{index}:#{escape_host_command(cmd)}"
|
392
|
+
RunLoop::Logging.log_debug(logger, cmd_str)
|
393
|
+
write_succeeded = false
|
394
|
+
2.times do |i|
|
395
|
+
RunLoop::Logging.log_debug(logger, "Trying write of command #{cmd_str} at index #{index}")
|
396
|
+
begin
|
397
|
+
RunLoop::Fifo.write(repl_path, cmd_str)
|
398
|
+
write_succeeded = validate_index_written(run_loop, index, logger)
|
399
|
+
rescue RunLoop::Fifo::NoReaderConfiguredError,
|
400
|
+
RunLoop::Fifo::WriteTimedOut => e
|
401
|
+
RunLoop::Logging.log_debug(logger, "Error while writing command (retry count #{i}). #{e}")
|
402
|
+
end
|
403
|
+
break if write_succeeded
|
404
|
+
end
|
405
|
+
unless write_succeeded
|
406
|
+
RunLoop::Logging.log_debug(logger, 'Failing...Raising RunLoop::WriteFailedError')
|
407
|
+
raise RunLoop::WriteFailedError.new("Trying write of command #{cmd_str} at index #{index}")
|
408
|
+
end
|
409
|
+
run_loop[:index] = index + 1
|
410
|
+
RunLoop::HostCache.default.write(run_loop) unless RunLoop::Environment.xtc?
|
411
|
+
index
|
412
|
+
end
|
413
|
+
|
414
|
+
def self.validate_index_written(run_loop, index, logger)
|
415
|
+
begin
|
416
|
+
Timeout::timeout(10, RunLoop::TimeoutError) do
|
417
|
+
Core.read_response(run_loop, index, 10, 'last_index')
|
418
|
+
end
|
419
|
+
RunLoop::Logging.log_debug(logger, "validate index written for index #{index} ok")
|
420
|
+
return true
|
421
|
+
rescue RunLoop::TimeoutError => _
|
422
|
+
RunLoop::Logging.log_debug(logger, "validate index written for index #{index} failed. Retrying.")
|
423
|
+
return false
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def self.escape_host_command(cmd)
|
428
|
+
backquote = "\\"
|
429
|
+
cmd.gsub(backquote,backquote*4)
|
430
|
+
end
|
431
|
+
|
432
|
+
def self.log_instruments_error(msg)
|
433
|
+
$stderr.puts "\033[31m\n\n*** #{msg} ***\n\n\033[0m"
|
434
|
+
$stderr.flush
|
435
|
+
end
|
436
|
+
|
437
|
+
def self.read_response(run_loop, expected_index, empty_file_timeout=10, search_for_property='index')
|
438
|
+
debug_read = RunLoop::Environment.debug_read?
|
439
|
+
|
440
|
+
log_file = run_loop[:log_file]
|
441
|
+
initial_offset = run_loop[:initial_offset] || 0
|
442
|
+
offset = initial_offset
|
443
|
+
|
444
|
+
result = nil
|
445
|
+
loop do
|
446
|
+
unless File.exist?(log_file) && File.size?(log_file)
|
447
|
+
sleep(0.2)
|
448
|
+
next
|
449
|
+
end
|
450
|
+
|
451
|
+
size = File.size(log_file)
|
452
|
+
output = File.read(log_file, size-offset, offset)
|
453
|
+
|
454
|
+
if /AXError: Could not auto-register for pid status change/.match(output)
|
455
|
+
if /kAXErrorServerNotFound/.match(output)
|
456
|
+
self.log_instruments_error('Accessibility is not enabled on device/simulator, please enable it.')
|
457
|
+
end
|
458
|
+
raise RunLoop::TimeoutError.new('AXError: Could not auto-register for pid status change')
|
459
|
+
end
|
460
|
+
|
461
|
+
if /Automation Instrument ran into an exception/.match(output)
|
462
|
+
raise RunLoop::TimeoutError.new('Exception while running script')
|
463
|
+
end
|
464
|
+
|
465
|
+
if /FBSOpenApplicationErrorDomain error/.match(output)
|
466
|
+
msg = "Instruments failed to launch app: 'FBSOpenApplicationErrorDomain error 8"
|
467
|
+
if RunLoop::Environment.debug?
|
468
|
+
self.log_instruments_error(msg)
|
469
|
+
end
|
470
|
+
raise RunLoop::TimeoutError.new(msg)
|
471
|
+
end
|
472
|
+
|
473
|
+
if /Error: Script threw an uncaught JavaScript error: unknown JavaScript exception/.match(output)
|
474
|
+
msg = "Instruments failed to launch: because of an unknown JavaScript exception"
|
475
|
+
if RunLoop::Environment.debug?
|
476
|
+
self.log_instruments_error(msg)
|
477
|
+
end
|
478
|
+
raise RunLoop::TimeoutError.new(msg)
|
479
|
+
end
|
480
|
+
|
481
|
+
index_if_found = output.index(START_DELIMITER)
|
482
|
+
if debug_read
|
483
|
+
puts output.gsub('*', '')
|
484
|
+
puts "Size #{size}"
|
485
|
+
puts "offset #{offset}"
|
486
|
+
puts "index_of #{START_DELIMITER}: #{index_if_found}"
|
487
|
+
end
|
488
|
+
|
489
|
+
if index_if_found
|
490
|
+
|
491
|
+
offset = offset + index_if_found
|
492
|
+
rest = output[index_if_found+START_DELIMITER.size..output.length]
|
493
|
+
index_of_json = rest.index("}#{END_DELIMITER}")
|
494
|
+
|
495
|
+
if index_of_json.nil?
|
496
|
+
#Wait for rest of json
|
497
|
+
sleep(0.1)
|
498
|
+
next
|
499
|
+
end
|
500
|
+
|
501
|
+
json = rest[0..index_of_json]
|
502
|
+
|
503
|
+
|
504
|
+
if debug_read
|
505
|
+
puts "Index #{index_if_found}, Size: #{size} Offset #{offset}"
|
506
|
+
|
507
|
+
puts ("parse #{json}")
|
508
|
+
end
|
509
|
+
|
510
|
+
offset = offset + json.size
|
511
|
+
parsed_result = JSON.parse(json)
|
512
|
+
if debug_read
|
513
|
+
p parsed_result
|
514
|
+
end
|
515
|
+
json_index_if_present = parsed_result[search_for_property]
|
516
|
+
if json_index_if_present && json_index_if_present == expected_index
|
517
|
+
result = parsed_result
|
518
|
+
break
|
519
|
+
end
|
520
|
+
else
|
521
|
+
sleep(0.1)
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
run_loop[:initial_offset] = offset
|
526
|
+
RunLoop::HostCache.default.write(run_loop) unless RunLoop::Environment.xtc?
|
527
|
+
result
|
528
|
+
end
|
529
|
+
|
530
|
+
def self.automation_template(instruments, candidate=RunLoop::Environment.trace_template)
|
531
|
+
unless candidate && File.exist?(candidate)
|
532
|
+
candidate = default_tracetemplate(instruments)
|
533
|
+
end
|
534
|
+
candidate
|
535
|
+
end
|
536
|
+
|
537
|
+
def self.default_tracetemplate(instruments=RunLoop::Instruments.new)
|
538
|
+
|
539
|
+
templates = instruments.templates
|
540
|
+
|
541
|
+
# xcrun instruments -s templates
|
542
|
+
# Xcode >= 6 will return known, Apple defined tracetemplates as names
|
543
|
+
# e.g. Automation, Zombies, Allocations
|
544
|
+
#
|
545
|
+
# Xcode < 6 will return known, Apple defined tracetemplates as paths.
|
546
|
+
#
|
547
|
+
# Xcode 6 Beta versions also return paths, but revert to 'normal'
|
548
|
+
# behavior when GM is released.
|
549
|
+
#
|
550
|
+
# Xcode 7 Beta versions appear to behavior like Xcode 6 Beta versions.
|
551
|
+
template = templates.find { |name| name == 'Automation' }
|
552
|
+
return template if template
|
553
|
+
|
554
|
+
candidate = templates.find do |path|
|
555
|
+
path =~ /\/Automation.tracetemplate/ and path =~ /Xcode/
|
556
|
+
end
|
557
|
+
|
558
|
+
if !candidate.nil?
|
559
|
+
return candidate.tr("\"", '').strip
|
560
|
+
end
|
561
|
+
|
562
|
+
message = ['Expected instruments to report an Automation tracetemplate.',
|
563
|
+
'Please report this as bug: https://github.com/calabash/run_loop/issues',
|
564
|
+
"In the bug report, include the output of:\n",
|
565
|
+
'$ xcrun xcodebuild -version',
|
566
|
+
"$ xcrun instruments -s templates\n"]
|
567
|
+
raise message.join("\n")
|
568
|
+
end
|
569
|
+
|
570
|
+
# @deprecated 2.1.0
|
571
|
+
# Replaced with Device.detect_physical_device_on_usb
|
572
|
+
def self.detect_connected_device
|
573
|
+
begin
|
574
|
+
Timeout::timeout(1, RunLoop::TimeoutError) do
|
575
|
+
return `#{File.join(SCRIPTS_PATH, 'udidetect')}`.chomp
|
576
|
+
end
|
577
|
+
rescue RunLoop::TimeoutError => _
|
578
|
+
`killall udidetect &> /dev/null`
|
579
|
+
end
|
580
|
+
nil
|
581
|
+
end
|
582
|
+
|
583
|
+
# @deprecated 2.1.0
|
584
|
+
# @!visibility private
|
585
|
+
# Are we targeting a simulator?
|
586
|
+
#
|
587
|
+
# @note The behavior of this method is different than the corresponding
|
588
|
+
# method in Calabash::Cucumber::Launcher method. If
|
589
|
+
# `:device_target => {nil | ''}`, then the calabash-ios method returns
|
590
|
+
# _false_. I am basing run-loop's behavior off the behavior in
|
591
|
+
# `self.udid_and_bundle_for_launcher`
|
592
|
+
#
|
593
|
+
# @see {Core::RunLoop.udid_and_bundle_for_launcher}
|
594
|
+
#
|
595
|
+
# @todo sim_control argument is no longer necessary and can be removed.
|
596
|
+
def self.simulator_target?(run_options, sim_control=nil)
|
597
|
+
# TODO Enable deprecation warning
|
598
|
+
# RunLoop.deprecated("2.1.0", "No replacement")
|
599
|
+
value = run_options[:device_target]
|
600
|
+
|
601
|
+
# Match the behavior of udid_and_bundle_for_launcher.
|
602
|
+
return true if value.nil? or value == ''
|
603
|
+
|
604
|
+
# 5.1 <= Xcode < 7.0
|
605
|
+
return true if value.downcase.include?('simulator')
|
606
|
+
|
607
|
+
# Not a physical device.
|
608
|
+
return false if value[DEVICE_UDID_REGEX, 0] != nil
|
609
|
+
|
610
|
+
# Check for named simulators and Xcode >= 7.0 simulators.
|
611
|
+
simctl = run_options[:sim_control] || run_options[:simctl] || RunLoop::Simctl.new
|
612
|
+
xcode = run_options[:xcode] || RunLoop::Xcode.new
|
613
|
+
simulator = simctl.simulators.find do |sim|
|
614
|
+
[
|
615
|
+
sim.instruments_identifier(xcode) == value,
|
616
|
+
sim.udid == value,
|
617
|
+
sim.name == value
|
618
|
+
].any?
|
619
|
+
end
|
620
|
+
!simulator.nil?
|
621
|
+
end
|
622
|
+
|
623
|
+
# @!visibility private
|
624
|
+
# @deprecated 2.1.0
|
625
|
+
#
|
626
|
+
# Do not call this method.
|
627
|
+
def self.udid_and_bundle_for_launcher(device_target, options, simctl=RunLoop::Simctl.new)
|
628
|
+
RunLoop.deprecated("2.1.0", "No replacement")
|
629
|
+
xcode = RunLoop::Xcode.new
|
630
|
+
|
631
|
+
bundle_dir_or_bundle_id = options[:app] || RunLoop::Environment.bundle_id || RunLoop::Environment.path_to_app_bundle
|
632
|
+
|
633
|
+
unless bundle_dir_or_bundle_id
|
634
|
+
raise 'key :app or environment variable APP_BUNDLE_PATH, BUNDLE_ID or APP must be specified as path to app bundle (simulator) or bundle id (device)'
|
635
|
+
end
|
636
|
+
|
637
|
+
if device_target.nil? || device_target.empty? || device_target == 'simulator'
|
638
|
+
device_target = self.default_simulator(xcode)
|
639
|
+
end
|
640
|
+
udid = device_target
|
641
|
+
|
642
|
+
unless self.simulator_target?(options)
|
643
|
+
bundle_dir_or_bundle_id = options[:bundle_id] if options[:bundle_id]
|
644
|
+
end
|
645
|
+
return udid, bundle_dir_or_bundle_id
|
646
|
+
end
|
647
|
+
|
648
|
+
# @deprecated 1.0.5
|
649
|
+
def self.ensure_instruments_not_running!
|
650
|
+
RunLoop::Instruments.new.kill_instruments
|
651
|
+
end
|
652
|
+
|
653
|
+
def self.instruments_running?
|
654
|
+
RunLoop::Instruments.new.instruments_running?
|
655
|
+
end
|
656
|
+
|
657
|
+
# @deprecated 1.0.5
|
658
|
+
def self.instruments_pids
|
659
|
+
RunLoop::Instruments.new.instruments_pids
|
660
|
+
end
|
661
|
+
|
662
|
+
# @deprecated 1.0.0 replaced with Xctools#version
|
663
|
+
def self.xcode_version(xcode=RunLoop::Xcode.new)
|
664
|
+
xcode.version
|
665
|
+
end
|
666
|
+
|
667
|
+
# @deprecated since 1.0.0
|
668
|
+
# still used extensively in calabash-ios launcher
|
669
|
+
def self.above_or_eql_version?(target_version, xcode_version)
|
670
|
+
if target_version.is_a?(RunLoop::Version)
|
671
|
+
target = target_version
|
672
|
+
else
|
673
|
+
target = RunLoop::Version.new(target_version)
|
674
|
+
end
|
675
|
+
|
676
|
+
if xcode_version.is_a?(RunLoop::Version)
|
677
|
+
xcode = xcode_version
|
678
|
+
else
|
679
|
+
xcode = RunLoop::Version.new(xcode_version)
|
680
|
+
end
|
681
|
+
target >= xcode
|
682
|
+
end
|
683
|
+
|
684
|
+
# @deprecated 1.0.5
|
685
|
+
def self.pids_for_run_loop(run_loop, &block)
|
686
|
+
RunLoop::Instruments.new.instruments_pids(&block)
|
687
|
+
end
|
688
|
+
|
689
|
+
private
|
690
|
+
|
691
|
+
# @!visibility private
|
692
|
+
#
|
693
|
+
# @param [Hash] options The launch options passed to .run_with_options
|
694
|
+
def self.prepare(run_options)
|
695
|
+
RunLoop::DotDir.rotate_result_directories
|
696
|
+
RunLoop::Instruments.rotate_cache_directories
|
697
|
+
true
|
698
|
+
end
|
699
|
+
|
700
|
+
# @!visibility private
|
701
|
+
#
|
702
|
+
# @param [RunLoop::Device] device The device under test.
|
703
|
+
# @param [RunLoop::Xcode] xcode The active Xcode
|
704
|
+
def self.default_uia_strategy(device, xcode)
|
705
|
+
if xcode.version_gte_7?
|
706
|
+
:host
|
707
|
+
elsif device.physical_device? && device.version >= RunLoop::Version.new("8.0")
|
708
|
+
:host
|
709
|
+
else
|
710
|
+
:preferences
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
# @!visibility private
|
715
|
+
#
|
716
|
+
# @param [Hash] options The launch options passed to .run_with_options
|
717
|
+
# @param [RunLoop::Device] device The device under test.
|
718
|
+
# @param [RunLoop::Xcode] xcode The active Xcode.
|
719
|
+
def self.detect_uia_strategy(options, device, xcode)
|
720
|
+
strategy = options[:uia_strategy] || self.default_uia_strategy(device, xcode)
|
721
|
+
|
722
|
+
if ![:host, :preferences, :shared_element].include?(strategy)
|
723
|
+
raise ArgumentError,
|
724
|
+
"Invalid strategy: expected '#{strategy}' to be :host, :preferences, or :shared_element"
|
725
|
+
end
|
726
|
+
strategy
|
727
|
+
end
|
728
|
+
|
729
|
+
# @!visibility private
|
730
|
+
#
|
731
|
+
# There is an unnatural relationship between the :script and the
|
732
|
+
# :uia_strategy keys.
|
733
|
+
#
|
734
|
+
# @param [Hash] options The launch options passed to .run_with_options
|
735
|
+
# @param [RunLoop::Device] device The device under test.
|
736
|
+
# @param [RunLoop::Xcode] xcode The active Xcode.
|
737
|
+
#
|
738
|
+
# @return [Hash] with two keys: :script and :uia_strategy
|
739
|
+
def self.detect_instruments_script_and_strategy(options, device, xcode)
|
740
|
+
strategy = options[:uia_strategy]
|
741
|
+
script = options[:script]
|
742
|
+
|
743
|
+
if script
|
744
|
+
script = self.expect_instruments_script(script)
|
745
|
+
if !strategy
|
746
|
+
strategy = :host
|
747
|
+
end
|
748
|
+
else
|
749
|
+
if strategy
|
750
|
+
script = self.instruments_script_for_uia_strategy(strategy)
|
751
|
+
else
|
752
|
+
if options[:calabash_lite]
|
753
|
+
strategy = :host
|
754
|
+
script = self.instruments_script_for_uia_strategy(strategy)
|
755
|
+
else
|
756
|
+
strategy = self.detect_uia_strategy(options, device, xcode)
|
757
|
+
script = self.instruments_script_for_uia_strategy(strategy)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
{
|
763
|
+
:script => script,
|
764
|
+
:strategy => strategy
|
765
|
+
}
|
766
|
+
end
|
767
|
+
|
768
|
+
# @!visibility private
|
769
|
+
#
|
770
|
+
# UIAutomation buffers log output in some very strange ways. RunLoop
|
771
|
+
# attempts to work around this buffering by forcing characters onto the
|
772
|
+
# UIALogger buffer. Once the buffer is full, UIAutomation will dump its
|
773
|
+
# contents. It is essential that the communication between UIAutomation
|
774
|
+
# and RunLoop be synchronized.
|
775
|
+
#
|
776
|
+
# Casual users should never set the :flush_uia_logs key; they should use the
|
777
|
+
# defaults.
|
778
|
+
#
|
779
|
+
# :no_flush is supported (for now) as alternative key.
|
780
|
+
#
|
781
|
+
# @param [Hash] options The launch options passed to .run_with_options
|
782
|
+
def self.detect_flush_uia_log_option(options)
|
783
|
+
if options.has_key?(:no_flush)
|
784
|
+
# Confusing.
|
785
|
+
# :no_flush == false means, flush the logs.
|
786
|
+
# :no_flush == true means, don't flush the logs.
|
787
|
+
return !options[:no_flush]
|
788
|
+
end
|
789
|
+
|
790
|
+
return options.fetch(:flush_uia_logs, true)
|
791
|
+
end
|
792
|
+
|
793
|
+
# @!visibility private
|
794
|
+
#
|
795
|
+
# @param [Hash] options The launch options passed to .run_with_options
|
796
|
+
def self.detect_reset_options(options)
|
797
|
+
return options[:reset] if options.has_key?(:reset)
|
798
|
+
|
799
|
+
return options[:reset_app_sandbox] if options.has_key?(:reset_app_sandbox)
|
800
|
+
|
801
|
+
RunLoop::Environment.reset_between_scenarios?
|
802
|
+
end
|
803
|
+
|
804
|
+
# Prepares the simulator for running.
|
805
|
+
#
|
806
|
+
# 1. enabling accessibility and software keyboard
|
807
|
+
# 2. installing / uninstalling apps
|
808
|
+
#
|
809
|
+
# TODO: move to CoreSimulator?
|
810
|
+
def self.prepare_simulator(app, device, xcode, simctl, reset_options)
|
811
|
+
|
812
|
+
# Validate the architecture.
|
813
|
+
self.expect_simulator_compatible_arch(device, app)
|
814
|
+
|
815
|
+
# Quits the simulator.
|
816
|
+
core_sim = RunLoop::CoreSimulator.new(device, app, :xcode => xcode)
|
817
|
+
|
818
|
+
# Calabash 0.x can only reset the app sandbox (true/false).
|
819
|
+
# Calabash 2.x has advanced reset options.
|
820
|
+
if reset_options
|
821
|
+
core_sim.reset_app_sandbox
|
822
|
+
end
|
823
|
+
|
824
|
+
# Will quit the simulator if it is running.
|
825
|
+
# @todo fix accessibility_enabled? so we don't have to quit the sim
|
826
|
+
# SimControl#accessibility_enabled? is always false during Core#prepare_simulator
|
827
|
+
# https://github.com/calabash/run_loop/issues/167
|
828
|
+
simctl.ensure_accessibility(device)
|
829
|
+
|
830
|
+
# Will quit the simulator if it is running.
|
831
|
+
# @todo fix software_keyboard_enabled? so we don't have to quit the sim
|
832
|
+
# SimControl#software_keyboard_enabled? is always false during Core#prepare_simulator
|
833
|
+
# https://github.com/calabash/run_loop/issues/168
|
834
|
+
simctl.ensure_software_keyboard(device)
|
835
|
+
|
836
|
+
# Launches the simulator if the app is not installed.
|
837
|
+
core_sim.install
|
838
|
+
|
839
|
+
# If CoreSimulator has already launched the simulator, it will not launch it again.
|
840
|
+
core_sim.launch_simulator
|
841
|
+
end
|
842
|
+
|
843
|
+
# @!visibility private
|
844
|
+
# Raise an error if the application binary is not compatible with the
|
845
|
+
# target simulator.
|
846
|
+
#
|
847
|
+
# @param [RunLoop::Device] device The device to install on.
|
848
|
+
# @param [RunLoop::App] app The app to install.
|
849
|
+
#
|
850
|
+
# @raise [RunLoop::IncompatibleArchitecture] Raises an error if the
|
851
|
+
# application binary is not compatible with the target simulator.
|
852
|
+
def self.expect_simulator_compatible_arch(device, app)
|
853
|
+
lipo = RunLoop::Lipo.new(app.path)
|
854
|
+
lipo.expect_compatible_arch(device)
|
855
|
+
|
856
|
+
RunLoop.log_debug("Simulator instruction set '#{device.instruction_set}' is compatible with '#{lipo.info}'")
|
857
|
+
end
|
858
|
+
|
859
|
+
# @!visibility private
|
860
|
+
def self.expect_instruments_script(script)
|
861
|
+
if script.is_a?(String)
|
862
|
+
unless File.exist?(script)
|
863
|
+
raise %Q[Expected instruments JavaScript file at path:
|
864
|
+
|
865
|
+
#{script}
|
866
|
+
|
867
|
+
Check the :script key in your launch options.]
|
868
|
+
end
|
869
|
+
script
|
870
|
+
elsif script.is_a?(Symbol)
|
871
|
+
path = self.script_for_key(script)
|
872
|
+
if !path
|
873
|
+
raise %Q[Expected :#{script} to be one of:
|
874
|
+
|
875
|
+
#{Core::SCRIPTS.keys.map { |key| ":#{key}" }.join("\n")}
|
876
|
+
|
877
|
+
Check the :script key in your launch options.]
|
878
|
+
end
|
879
|
+
path
|
880
|
+
else
|
881
|
+
raise %Q[Expected '#{script}' to be a Symbol or a String.
|
882
|
+
|
883
|
+
Check the :script key in your launch options.]
|
884
|
+
end
|
885
|
+
end
|
886
|
+
|
887
|
+
# @!visibility private
|
888
|
+
def self.instruments_script_for_uia_strategy(uia_strategy)
|
889
|
+
case uia_strategy
|
890
|
+
when :preferences
|
891
|
+
self.script_for_key(:run_loop_fast_uia)
|
892
|
+
when :host
|
893
|
+
self.script_for_key(:run_loop_host)
|
894
|
+
when :shared_element
|
895
|
+
self.script_for_key(:run_loop_shared_element)
|
896
|
+
else
|
897
|
+
self.script_for_key(:run_loop_basic)
|
898
|
+
end
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
|