run_loop 2.1.7 → 2.1.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/core_simulator.rb +76 -3
- data/lib/run_loop/device_agent/Frameworks.zip +0 -0
- data/lib/run_loop/device_agent/app/DeviceAgent-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/bin/CLI.json +125 -0
- data/lib/run_loop/device_agent/bin/iOSDeviceManager +0 -0
- data/lib/run_loop/device_agent/client.rb +853 -0
- data/lib/run_loop/device_agent/ios_device_manager.rb +97 -32
- data/lib/run_loop/device_agent/ipa/DeviceAgent-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/{launcher.rb → launcher_strategy.rb} +8 -8
- data/lib/run_loop/device_agent/{cbxrunner.rb → runner.rb} +15 -25
- data/lib/run_loop/device_agent/xcodebuild.rb +25 -8
- data/lib/run_loop/environment.rb +10 -6
- data/lib/run_loop/http/retriable_client.rb +7 -1
- data/lib/run_loop/simctl.rb +24 -15
- data/lib/run_loop/version.rb +7 -2
- data/lib/run_loop/xcode.rb +2 -2
- data/lib/run_loop.rb +13 -13
- metadata +10 -7
- data/lib/run_loop/device_agent/app/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/device_agent/ipa/CBX-Runner.app.zip +0 -0
- data/lib/run_loop/xcuitest.rb +0 -727
@@ -0,0 +1,853 @@
|
|
1
|
+
module RunLoop
|
2
|
+
|
3
|
+
# @!visibility private
|
4
|
+
module DeviceAgent
|
5
|
+
|
6
|
+
# @!visibility private
|
7
|
+
class Client
|
8
|
+
|
9
|
+
require "run_loop/shell"
|
10
|
+
include RunLoop::Shell
|
11
|
+
|
12
|
+
require "run_loop/encoding"
|
13
|
+
include RunLoop::Encoding
|
14
|
+
|
15
|
+
require "run_loop/cache"
|
16
|
+
|
17
|
+
class HTTPError < RuntimeError; end
|
18
|
+
|
19
|
+
# @!visibility private
|
20
|
+
#
|
21
|
+
# These defaults may change at any time.
|
22
|
+
DEFAULTS = {
|
23
|
+
:port => 27753,
|
24
|
+
:simulator_ip => "127.0.0.1",
|
25
|
+
:http_timeout => RunLoop::Environment.ci? ? 120 : 10,
|
26
|
+
:route_version => "1.0",
|
27
|
+
:shutdown_device_agent_before_launch => false
|
28
|
+
}
|
29
|
+
|
30
|
+
# @!visibility private
|
31
|
+
def self.run(options={})
|
32
|
+
# logger = options[:logger]
|
33
|
+
simctl = options[:sim_control] || options[:simctl] || RunLoop::Simctl.new
|
34
|
+
xcode = options[:xcode] || RunLoop::Xcode.new
|
35
|
+
instruments = options[:instruments] || RunLoop::Instruments.new
|
36
|
+
|
37
|
+
# Find the Device under test, the App under test, and reset options.
|
38
|
+
device = RunLoop::Device.detect_device(options, xcode, simctl, instruments)
|
39
|
+
app_details = RunLoop::DetectAUT.detect_app_under_test(options)
|
40
|
+
reset_options = RunLoop::Core.send(:detect_reset_options, options)
|
41
|
+
|
42
|
+
app = app_details[:app]
|
43
|
+
bundle_id = app_details[:bundle_id]
|
44
|
+
|
45
|
+
if device.simulator? && app
|
46
|
+
core_sim = RunLoop::CoreSimulator.new(device, app, :xcode => xcode)
|
47
|
+
if reset_options
|
48
|
+
core_sim.reset_app_sandbox
|
49
|
+
end
|
50
|
+
|
51
|
+
simctl.ensure_software_keyboard(device)
|
52
|
+
core_sim.install
|
53
|
+
end
|
54
|
+
|
55
|
+
cbx_launcher = Client.detect_cbx_launcher(options, device)
|
56
|
+
|
57
|
+
code_sign_identity = options[:code_sign_identity]
|
58
|
+
if !code_sign_identity
|
59
|
+
code_sign_identity = RunLoop::Environment::code_sign_identity
|
60
|
+
end
|
61
|
+
|
62
|
+
if device.physical_device? && cbx_launcher.name == :ios_device_manager
|
63
|
+
if !code_sign_identity
|
64
|
+
raise RuntimeError, %Q[
|
65
|
+
Targeting a physical devices requires a code signing identity.
|
66
|
+
|
67
|
+
Rerun your test with:
|
68
|
+
|
69
|
+
$ CODE_SIGN_IDENTITY="iPhone Developer: Your Name (ABCDEF1234)" cucumber
|
70
|
+
|
71
|
+
To see the valid code signing identities on your device run:
|
72
|
+
|
73
|
+
$ xcrun security find-identity -v -p codesigning
|
74
|
+
|
75
|
+
]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
launch_options = options.merge({:code_sign_identity => code_sign_identity})
|
80
|
+
xcuitest = RunLoop::DeviceAgent::Client.new(bundle_id, device, cbx_launcher)
|
81
|
+
xcuitest.launch(launch_options)
|
82
|
+
|
83
|
+
if !RunLoop::Environment.xtc?
|
84
|
+
cache = {
|
85
|
+
:cbx_launcher => cbx_launcher.name,
|
86
|
+
:udid => device.udid,
|
87
|
+
:app => bundle_id,
|
88
|
+
:gesture_performer => :device_agent,
|
89
|
+
:code_sign_identity => code_sign_identity
|
90
|
+
}
|
91
|
+
RunLoop::Cache.default.write(cache)
|
92
|
+
end
|
93
|
+
xcuitest
|
94
|
+
end
|
95
|
+
|
96
|
+
# @!visibility private
|
97
|
+
#
|
98
|
+
# @param [RunLoop::Device] device the device under test
|
99
|
+
def self.default_cbx_launcher(device)
|
100
|
+
RunLoop::DeviceAgent::IOSDeviceManager.new(device)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @!visibility private
|
104
|
+
# @param [Hash] options the options passed by the user
|
105
|
+
# @param [RunLoop::Device] device the device under test
|
106
|
+
def self.detect_cbx_launcher(options, device)
|
107
|
+
value = options[:cbx_launcher]
|
108
|
+
if value
|
109
|
+
if value == :xcodebuild
|
110
|
+
RunLoop::DeviceAgent::Xcodebuild.new(device)
|
111
|
+
elsif value == :ios_device_manager
|
112
|
+
RunLoop::DeviceAgent::IOSDeviceManager.new(device)
|
113
|
+
else
|
114
|
+
raise(ArgumentError,
|
115
|
+
"Expected :cbx_launcher => #{value} to be :xcodebuild or :ios_device_manager")
|
116
|
+
end
|
117
|
+
else
|
118
|
+
Client.default_cbx_launcher(device)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
attr_reader :bundle_id, :device, :cbx_launcher, :launch_options
|
123
|
+
|
124
|
+
# @!visibility private
|
125
|
+
#
|
126
|
+
# The app with `bundle_id` needs to be installed.
|
127
|
+
#
|
128
|
+
# @param [String] bundle_id The identifier of the app under test.
|
129
|
+
# @param [RunLoop::Device] device The device under test.
|
130
|
+
# @param [RunLoop::DeviceAgent::LauncherStrategy] cbx_launcher The entity that
|
131
|
+
# launches the CBXRunner.
|
132
|
+
def initialize(bundle_id, device, cbx_launcher)
|
133
|
+
@bundle_id = bundle_id
|
134
|
+
@device = device
|
135
|
+
@cbx_launcher = cbx_launcher
|
136
|
+
end
|
137
|
+
|
138
|
+
# @!visibility private
|
139
|
+
def to_s
|
140
|
+
"#<DeviceAgent #{url} : #{bundle_id} : #{device} : #{cbx_launcher}>"
|
141
|
+
end
|
142
|
+
|
143
|
+
# @!visibility private
|
144
|
+
def inspect
|
145
|
+
to_s
|
146
|
+
end
|
147
|
+
|
148
|
+
# @!visibility private
|
149
|
+
def launch(options={})
|
150
|
+
@launch_options = options
|
151
|
+
start = Time.now
|
152
|
+
launch_cbx_runner(options)
|
153
|
+
launch_aut
|
154
|
+
elapsed = Time.now - start
|
155
|
+
RunLoop.log_debug("Took #{elapsed} seconds to launch #{bundle_id} on #{device}")
|
156
|
+
true
|
157
|
+
end
|
158
|
+
|
159
|
+
# @!visibility private
|
160
|
+
def running?
|
161
|
+
begin
|
162
|
+
health(ping_options)
|
163
|
+
rescue => _
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# @!visibility private
|
169
|
+
def stop
|
170
|
+
begin
|
171
|
+
shutdown
|
172
|
+
rescue => _
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# @!visibility private
|
178
|
+
def launch_other_app(bundle_id)
|
179
|
+
launch_aut(bundle_id)
|
180
|
+
end
|
181
|
+
|
182
|
+
# @!visibility private
|
183
|
+
def device_info
|
184
|
+
options = http_options
|
185
|
+
request = request("device")
|
186
|
+
client = client(options)
|
187
|
+
response = client.get(request)
|
188
|
+
expect_200_response(response)
|
189
|
+
end
|
190
|
+
|
191
|
+
# TODO Legacy API; remove once this branch is merged:
|
192
|
+
# https://github.com/calabash/DeviceAgent.iOS/pull/133
|
193
|
+
alias_method :runtime, :device_info
|
194
|
+
|
195
|
+
# @!visibility private
|
196
|
+
def server_pid
|
197
|
+
options = http_options
|
198
|
+
request = request("pid")
|
199
|
+
client = client(options)
|
200
|
+
response = client.get(request)
|
201
|
+
expect_200_response(response)
|
202
|
+
end
|
203
|
+
|
204
|
+
# @!visibility private
|
205
|
+
def server_version
|
206
|
+
options = http_options
|
207
|
+
request = request("version")
|
208
|
+
client = client(options)
|
209
|
+
response = client.get(request)
|
210
|
+
expect_200_response(response)
|
211
|
+
end
|
212
|
+
|
213
|
+
# @!visibility private
|
214
|
+
def session_identifier
|
215
|
+
options = http_options
|
216
|
+
request = request("sessionIdentifier")
|
217
|
+
client = client(options)
|
218
|
+
response = client.get(request)
|
219
|
+
expect_200_response(response)
|
220
|
+
end
|
221
|
+
|
222
|
+
# @!visibility private
|
223
|
+
def tree
|
224
|
+
options = http_options
|
225
|
+
request = request("tree")
|
226
|
+
client = client(options)
|
227
|
+
response = client.get(request)
|
228
|
+
expect_200_response(response)
|
229
|
+
end
|
230
|
+
|
231
|
+
# @!visibility private
|
232
|
+
def keyboard_visible?
|
233
|
+
options = http_options
|
234
|
+
parameters = { :type => "Keyboard" }
|
235
|
+
request = request("query", parameters)
|
236
|
+
client = client(options)
|
237
|
+
response = client.post(request)
|
238
|
+
hash = expect_200_response(response)
|
239
|
+
result = hash["result"]
|
240
|
+
result.count != 0
|
241
|
+
end
|
242
|
+
|
243
|
+
# @!visibility private
|
244
|
+
def enter_text(string)
|
245
|
+
if !keyboard_visible?
|
246
|
+
raise RuntimeError, "Keyboard must be visible"
|
247
|
+
end
|
248
|
+
options = http_options
|
249
|
+
parameters = {
|
250
|
+
:gesture => "enter_text",
|
251
|
+
:options => {
|
252
|
+
:string => string
|
253
|
+
}
|
254
|
+
}
|
255
|
+
request = request("gesture", parameters)
|
256
|
+
client = client(options)
|
257
|
+
response = client.post(request)
|
258
|
+
expect_200_response(response)
|
259
|
+
end
|
260
|
+
|
261
|
+
# @!visibility private
|
262
|
+
def query(mark, options={})
|
263
|
+
default_options = {
|
264
|
+
all: false,
|
265
|
+
specifier: :id
|
266
|
+
}
|
267
|
+
merged_options = default_options.merge(options)
|
268
|
+
|
269
|
+
parameters = { merged_options[:specifier] => mark }
|
270
|
+
request = request("query", parameters)
|
271
|
+
client = client(http_options)
|
272
|
+
|
273
|
+
RunLoop.log_debug %Q[Sending query with parameters:
|
274
|
+
|
275
|
+
#{JSON.pretty_generate(parameters)}
|
276
|
+
|
277
|
+
]
|
278
|
+
|
279
|
+
response = client.post(request)
|
280
|
+
hash = expect_200_response(response)
|
281
|
+
elements = hash["result"]
|
282
|
+
|
283
|
+
if merged_options[:all]
|
284
|
+
elements
|
285
|
+
else
|
286
|
+
elements.select do |element|
|
287
|
+
element["hitable"]
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# @!visibility private
|
293
|
+
def alert_visible?
|
294
|
+
parameters = { :type => "Alert" }
|
295
|
+
request = request("query", parameters)
|
296
|
+
client = client(http_options)
|
297
|
+
response = client.post(request)
|
298
|
+
hash = expect_200_response(response)
|
299
|
+
!hash["result"].empty?
|
300
|
+
end
|
301
|
+
|
302
|
+
# @!visibility private
|
303
|
+
def query_for_coordinate(mark)
|
304
|
+
elements = query(mark)
|
305
|
+
coordinate_from_query_result(elements)
|
306
|
+
end
|
307
|
+
|
308
|
+
# @!visibility private
|
309
|
+
def touch(mark, options={})
|
310
|
+
coordinate = query_for_coordinate(mark)
|
311
|
+
perform_coordinate_gesture("touch",
|
312
|
+
coordinate[:x], coordinate[:y],
|
313
|
+
options)
|
314
|
+
end
|
315
|
+
|
316
|
+
alias_method :tap, :touch
|
317
|
+
|
318
|
+
# @!visibility private
|
319
|
+
def double_tap(mark, options={})
|
320
|
+
coordinate = query_for_coordinate(mark)
|
321
|
+
perform_coordinate_gesture("double_tap",
|
322
|
+
coordinate[:x], coordinate[:y],
|
323
|
+
options)
|
324
|
+
end
|
325
|
+
|
326
|
+
# @!visibility private
|
327
|
+
def two_finger_tap(mark, options={})
|
328
|
+
coordinate = query_for_coordinate(mark)
|
329
|
+
perform_coordinate_gesture("two_finger_tap",
|
330
|
+
coordinate[:x], coordinate[:y],
|
331
|
+
options)
|
332
|
+
end
|
333
|
+
|
334
|
+
# @!visibility private
|
335
|
+
def rotate_home_button_to(position, sleep_for=1.0)
|
336
|
+
orientation = normalize_orientation_position(position)
|
337
|
+
parameters = {
|
338
|
+
:orientation => orientation
|
339
|
+
}
|
340
|
+
request = request("rotate_home_button_to", parameters)
|
341
|
+
client = client(http_options)
|
342
|
+
response = client.post(request)
|
343
|
+
json = expect_200_response(response)
|
344
|
+
sleep(sleep_for)
|
345
|
+
json
|
346
|
+
end
|
347
|
+
|
348
|
+
# @!visibility private
|
349
|
+
def pan_between_coordinates(start_point, end_point, options={})
|
350
|
+
default_options = {
|
351
|
+
:num_fingers => 1,
|
352
|
+
:duration => 0.5
|
353
|
+
}
|
354
|
+
|
355
|
+
merged_options = default_options.merge(options)
|
356
|
+
|
357
|
+
parameters = {
|
358
|
+
:gesture => "drag",
|
359
|
+
:specifiers => {
|
360
|
+
:coordinates => [start_point, end_point]
|
361
|
+
},
|
362
|
+
:options => merged_options
|
363
|
+
}
|
364
|
+
|
365
|
+
make_gesture_request(parameters)
|
366
|
+
end
|
367
|
+
|
368
|
+
# @!visibility private
|
369
|
+
def perform_coordinate_gesture(gesture, x, y, options={})
|
370
|
+
parameters = {
|
371
|
+
:gesture => gesture,
|
372
|
+
:specifiers => {
|
373
|
+
:coordinate => {x: x, y: y}
|
374
|
+
},
|
375
|
+
:options => options
|
376
|
+
}
|
377
|
+
|
378
|
+
make_gesture_request(parameters)
|
379
|
+
end
|
380
|
+
|
381
|
+
# @!visibility private
|
382
|
+
def make_gesture_request(parameters)
|
383
|
+
|
384
|
+
RunLoop.log_debug %Q[Sending request to perform '#{parameters[:gesture]}' with:
|
385
|
+
|
386
|
+
#{JSON.pretty_generate(parameters)}
|
387
|
+
|
388
|
+
]
|
389
|
+
request = request("gesture", parameters)
|
390
|
+
client = client(http_options)
|
391
|
+
response = client.post(request)
|
392
|
+
expect_200_response(response)
|
393
|
+
end
|
394
|
+
|
395
|
+
# @!visibility private
|
396
|
+
def coordinate_from_query_result(matches)
|
397
|
+
|
398
|
+
if matches.nil? || matches.empty?
|
399
|
+
raise "Expected #{hash} to contain some results"
|
400
|
+
end
|
401
|
+
|
402
|
+
rect = matches.first["rect"]
|
403
|
+
h = rect["height"]
|
404
|
+
w = rect["width"]
|
405
|
+
x = rect["x"]
|
406
|
+
y = rect["y"]
|
407
|
+
|
408
|
+
touchx = x + (w/2.0)
|
409
|
+
touchy = y + (h/2.0)
|
410
|
+
|
411
|
+
new_rect = rect.dup
|
412
|
+
new_rect[:center_x] = touchx
|
413
|
+
new_rect[:center_y] = touchy
|
414
|
+
|
415
|
+
RunLoop.log_debug(%Q[Rect from query:
|
416
|
+
|
417
|
+
#{JSON.pretty_generate(new_rect)}
|
418
|
+
|
419
|
+
])
|
420
|
+
{:x => touchx,
|
421
|
+
:y => touchy}
|
422
|
+
end
|
423
|
+
|
424
|
+
|
425
|
+
# @!visibility private
|
426
|
+
def change_volume(up_or_down)
|
427
|
+
string = up_or_down.to_s
|
428
|
+
parameters = {
|
429
|
+
:volume => string
|
430
|
+
}
|
431
|
+
request = request("volume", parameters)
|
432
|
+
client = client(http_options)
|
433
|
+
response = client.post(request)
|
434
|
+
json = expect_200_response(response)
|
435
|
+
# Set in the route
|
436
|
+
sleep(0.2)
|
437
|
+
json
|
438
|
+
end
|
439
|
+
|
440
|
+
private
|
441
|
+
|
442
|
+
# @!visibility private
|
443
|
+
def xcrun
|
444
|
+
RunLoop::Xcrun.new
|
445
|
+
end
|
446
|
+
|
447
|
+
# @!visibility private
|
448
|
+
def url
|
449
|
+
@url ||= detect_device_agent_url
|
450
|
+
end
|
451
|
+
|
452
|
+
# @!visibility private
|
453
|
+
def detect_device_agent_url
|
454
|
+
url_from_environment ||
|
455
|
+
url_for_simulator ||
|
456
|
+
url_from_device_endpoint ||
|
457
|
+
url_from_device_name
|
458
|
+
end
|
459
|
+
|
460
|
+
# @!visibility private
|
461
|
+
def url_from_environment
|
462
|
+
url = RunLoop::Environment.device_agent_url
|
463
|
+
return if url.nil?
|
464
|
+
|
465
|
+
if url.end_with?("/")
|
466
|
+
url
|
467
|
+
else
|
468
|
+
"#{url}/"
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# @!visibility private
|
473
|
+
def url_for_simulator
|
474
|
+
if device.simulator?
|
475
|
+
"http://#{DEFAULTS[:simulator_ip]}:#{DEFAULTS[:port]}/"
|
476
|
+
else
|
477
|
+
nil
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# @!visibility private
|
482
|
+
def url_from_device_endpoint
|
483
|
+
calabash_endpoint = RunLoop::Environment.device_endpoint
|
484
|
+
if calabash_endpoint
|
485
|
+
base = calabash_endpoint.split(":")[0..1].join(":")
|
486
|
+
"#{base}:#{DEFAULTS[:port]}/"
|
487
|
+
else
|
488
|
+
nil
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# @!visibility private
|
493
|
+
# TODO This block is not well tested
|
494
|
+
# TODO extract to a module; Calabash can use to detect device endpoint
|
495
|
+
def url_from_device_name
|
496
|
+
# Transforms the default "Joshua's iPhone" to a DNS name.
|
497
|
+
device_name = device.name.gsub(/[']/, "").gsub(/[\s]/, "-")
|
498
|
+
|
499
|
+
# Replace diacritic markers and unknown characters.
|
500
|
+
transliterated = transliterate(device_name).tr("?", "")
|
501
|
+
|
502
|
+
# Anything that cannot be transliterated is a ?
|
503
|
+
replaced = transliterated.tr("?", "")
|
504
|
+
|
505
|
+
"http://#{replaced}.local:#{DEFAULTS[:port]}/"
|
506
|
+
end
|
507
|
+
|
508
|
+
# @!visibility private
|
509
|
+
def server
|
510
|
+
@server ||= RunLoop::HTTP::Server.new(url)
|
511
|
+
end
|
512
|
+
|
513
|
+
# @!visibility private
|
514
|
+
def client(options={})
|
515
|
+
RunLoop::HTTP::RetriableClient.new(server, options)
|
516
|
+
end
|
517
|
+
|
518
|
+
# @!visibility private
|
519
|
+
def versioned_route(route)
|
520
|
+
"#{DEFAULTS[:route_version]}/#{route}"
|
521
|
+
end
|
522
|
+
|
523
|
+
# @!visibility private
|
524
|
+
def request(route, parameters={})
|
525
|
+
versioned = versioned_route(route)
|
526
|
+
RunLoop::HTTP::Request.request(versioned, parameters)
|
527
|
+
end
|
528
|
+
|
529
|
+
# @!visibility private
|
530
|
+
def ping_options
|
531
|
+
@ping_options ||= { :timeout => 0.5, :retries => 1 }
|
532
|
+
end
|
533
|
+
|
534
|
+
# @!visibility private
|
535
|
+
def http_options
|
536
|
+
if cbx_launcher.name == :xcodebuild
|
537
|
+
timeout = DEFAULTS[:http_timeout] * 2
|
538
|
+
{
|
539
|
+
:timeout => timeout,
|
540
|
+
:interval => 0.1,
|
541
|
+
:retries => (timeout/0.1).to_i
|
542
|
+
}
|
543
|
+
else
|
544
|
+
{
|
545
|
+
:timeout => DEFAULTS[:http_timeout],
|
546
|
+
:interval => 0.1,
|
547
|
+
:retries => (DEFAULTS[:http_timeout]/0.1).to_i
|
548
|
+
}
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
# @!visibility private
|
553
|
+
def session_delete
|
554
|
+
# https://xamarin.atlassian.net/browse/TCFW-255
|
555
|
+
# httpclient is unable to send a valid DELETE
|
556
|
+
args = ["curl", "-X", "DELETE", %Q[#{url}#{versioned_route("session")}]]
|
557
|
+
run_shell_command(args, {:log_cmd => true})
|
558
|
+
|
559
|
+
# options = ping_options
|
560
|
+
# request = request("session")
|
561
|
+
# client = client(options)
|
562
|
+
# begin
|
563
|
+
# response = client.delete(request)
|
564
|
+
# body = expect_200_response(response)
|
565
|
+
# RunLoop.log_debug("CBX-Runner says, #{body}")
|
566
|
+
# body
|
567
|
+
# rescue => e
|
568
|
+
# RunLoop.log_debug("CBX-Runner session delete error: #{e}")
|
569
|
+
# nil
|
570
|
+
# end
|
571
|
+
end
|
572
|
+
|
573
|
+
# @!visibility private
|
574
|
+
# TODO expect 200 response and parse body (atm the body in not valid JSON)
|
575
|
+
def shutdown
|
576
|
+
session_delete
|
577
|
+
options = ping_options
|
578
|
+
request = request("shutdown")
|
579
|
+
client = client(options)
|
580
|
+
body = nil
|
581
|
+
begin
|
582
|
+
response = client.post(request)
|
583
|
+
body = response.body
|
584
|
+
RunLoop.log_debug("DeviceAgent-Runner says, \"#{body}\"")
|
585
|
+
|
586
|
+
now = Time.now
|
587
|
+
poll_until = now + 10.0
|
588
|
+
running = true
|
589
|
+
while Time.now < poll_until
|
590
|
+
running = !running?
|
591
|
+
break if running
|
592
|
+
sleep(0.1)
|
593
|
+
end
|
594
|
+
|
595
|
+
RunLoop.log_debug("Waited for #{Time.now - now} seconds for DeviceAgent to shutdown")
|
596
|
+
body
|
597
|
+
rescue => e
|
598
|
+
RunLoop.log_debug("DeviceAgent-Runner shutdown error: #{e}")
|
599
|
+
ensure
|
600
|
+
quit_options = { :timeout => 0.5 }
|
601
|
+
term_options = { :timeout => 0.5 }
|
602
|
+
kill_options = { :timeout => 0.5 }
|
603
|
+
|
604
|
+
process_name = "iOSDeviceManager"
|
605
|
+
RunLoop::ProcessWaiter.new(process_name).pids.each do |pid|
|
606
|
+
quit = RunLoop::ProcessTerminator.new(pid, "QUIT", process_name, quit_options)
|
607
|
+
if !quit.kill_process
|
608
|
+
term = RunLoop::ProcessTerminator.new(pid, "TERM", process_name, term_options)
|
609
|
+
if !term.kill_process
|
610
|
+
kill = RunLoop::ProcessTerminator.new(pid, "KILL", process_name, kill_options)
|
611
|
+
kill.kill_process
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
body
|
617
|
+
end
|
618
|
+
|
619
|
+
# @!visibility private
|
620
|
+
# TODO expect 200 response and parse body (atm the body is not valid JSON)
|
621
|
+
def health(options={})
|
622
|
+
merged_options = http_options.merge(options)
|
623
|
+
request = request("health")
|
624
|
+
client = client(merged_options)
|
625
|
+
response = client.get(request)
|
626
|
+
body = response.body
|
627
|
+
RunLoop.log_debug("CBX-Runner driver says, \"#{body}\"")
|
628
|
+
body
|
629
|
+
end
|
630
|
+
|
631
|
+
|
632
|
+
# TODO cbx_runner_stale? returns false always
|
633
|
+
def cbx_runner_stale?
|
634
|
+
false
|
635
|
+
# The RunLoop::Version class needs to be updated to handle timestamps.
|
636
|
+
#
|
637
|
+
# if cbx_launcher.name == :xcodebuild
|
638
|
+
# return false
|
639
|
+
# end
|
640
|
+
|
641
|
+
# version_info = server_version
|
642
|
+
# running_bundle_version = RunLoop::Version.new(version_info[:bundle_version])
|
643
|
+
# bundle_version = RunLoop::App.new(cbx_launcher.runner.runner).bundle_version
|
644
|
+
#
|
645
|
+
# running_bundle_version < bundle_version
|
646
|
+
end
|
647
|
+
|
648
|
+
# @!visibility private
|
649
|
+
def launch_cbx_runner(options={})
|
650
|
+
merged_options = DEFAULTS.merge(options)
|
651
|
+
|
652
|
+
if merged_options[:shutdown_device_agent_before_launch]
|
653
|
+
RunLoop.log_debug("Launch options insist that the DeviceAgent be shutdown")
|
654
|
+
shutdown
|
655
|
+
|
656
|
+
if cbx_launcher.name == :xcodebuild
|
657
|
+
sleep(5.0)
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
if running?
|
662
|
+
RunLoop.log_debug("DeviceAgent is already running")
|
663
|
+
if cbx_runner_stale?
|
664
|
+
shutdown
|
665
|
+
else
|
666
|
+
# TODO: is it necessary to return the pid? Or can we return true?
|
667
|
+
return server_pid
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
if cbx_launcher.name == :xcodebuild
|
672
|
+
RunLoop.log_debug("xcodebuild is the launcher - terminating existing xcodebuild processes")
|
673
|
+
term_options = { :timeout => 0.5 }
|
674
|
+
kill_options = { :timeout => 0.5 }
|
675
|
+
RunLoop::ProcessWaiter.new("xcodebuild").pids.each do |pid|
|
676
|
+
term = RunLoop::ProcessTerminator.new(pid, 'TERM', "xcodebuild", term_options)
|
677
|
+
killed = term.kill_process
|
678
|
+
unless killed
|
679
|
+
RunLoop::ProcessTerminator.new(pid, 'KILL', "xcodebuild", kill_options)
|
680
|
+
end
|
681
|
+
end
|
682
|
+
sleep(2.0)
|
683
|
+
end
|
684
|
+
|
685
|
+
start = Time.now
|
686
|
+
RunLoop.log_debug("Waiting for CBX-Runner to launch...")
|
687
|
+
pid = cbx_launcher.launch(options)
|
688
|
+
|
689
|
+
if cbx_launcher.name == :xcodebuild
|
690
|
+
sleep(2.0)
|
691
|
+
end
|
692
|
+
|
693
|
+
begin
|
694
|
+
timeout = RunLoop::Environment.ci? ? 120 : 60
|
695
|
+
health_options = {
|
696
|
+
:timeout => timeout,
|
697
|
+
:interval => 0.1,
|
698
|
+
:retries => (timeout/0.1).to_i
|
699
|
+
}
|
700
|
+
|
701
|
+
health(health_options)
|
702
|
+
rescue RunLoop::HTTP::Error => _
|
703
|
+
raise %Q[
|
704
|
+
|
705
|
+
Could not connect to the DeviceAgent service.
|
706
|
+
|
707
|
+
device: #{device}
|
708
|
+
url: #{url}
|
709
|
+
|
710
|
+
To diagnose the problem tail the launcher log file:
|
711
|
+
|
712
|
+
$ tail -1000 -F #{cbx_launcher.class.log_file}
|
713
|
+
|
714
|
+
]
|
715
|
+
end
|
716
|
+
|
717
|
+
RunLoop.log_debug("Took #{Time.now - start} launch and respond to /health")
|
718
|
+
|
719
|
+
# TODO: is it necessary to return the pid? Or can we return true?
|
720
|
+
pid
|
721
|
+
end
|
722
|
+
|
723
|
+
# @!visibility private
|
724
|
+
def launch_aut(bundle_id = @bundle_id)
|
725
|
+
client = client(http_options)
|
726
|
+
request = request("session", {:bundleID => bundle_id})
|
727
|
+
|
728
|
+
if device.simulator?
|
729
|
+
# Yes, we could use iOSDeviceManager to check, I dont understand the
|
730
|
+
# behavior yet - does it require the simulator be launched?
|
731
|
+
# CoreSimulator can check without launching the simulator.
|
732
|
+
installed = CoreSimulator.app_installed?(device, bundle_id)
|
733
|
+
else
|
734
|
+
if cbx_launcher.name == :xcodebuild
|
735
|
+
# :xcodebuild users are on their own.
|
736
|
+
RunLoop.log_debug("Detected :xcodebuild launcher; skipping app installed check")
|
737
|
+
installed = true
|
738
|
+
else
|
739
|
+
installed = cbx_launcher.app_installed?(bundle_id)
|
740
|
+
end
|
741
|
+
end
|
742
|
+
|
743
|
+
if !installed
|
744
|
+
raise RuntimeError, %Q[
|
745
|
+
The app you are trying to launch is not installed on the target device:
|
746
|
+
|
747
|
+
bundle identifier: #{bundle_id}
|
748
|
+
device: #{device}
|
749
|
+
|
750
|
+
Please install it.
|
751
|
+
|
752
|
+
]
|
753
|
+
end
|
754
|
+
|
755
|
+
begin
|
756
|
+
response = client.post(request)
|
757
|
+
RunLoop.log_debug("Launched #{bundle_id} on #{device}")
|
758
|
+
RunLoop.log_debug("#{response.body}")
|
759
|
+
if device.simulator?
|
760
|
+
# It is not clear yet whether we should do this. There is a problem
|
761
|
+
# in the simulator_wait_for_stable_state; it waits too long.
|
762
|
+
# device.simulator_wait_for_stable_state
|
763
|
+
end
|
764
|
+
expect_200_response(response)
|
765
|
+
rescue => e
|
766
|
+
raise e.class, %Q[
|
767
|
+
|
768
|
+
Could not launch #{bundle_id} on #{device}:
|
769
|
+
|
770
|
+
#{e.message}
|
771
|
+
|
772
|
+
Something went wrong.
|
773
|
+
|
774
|
+
]
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
# @!visibility private
|
779
|
+
def response_body_to_hash(response)
|
780
|
+
body = response.body
|
781
|
+
begin
|
782
|
+
JSON.parse(body)
|
783
|
+
rescue TypeError, JSON::ParserError => _
|
784
|
+
raise RunLoop::DeviceAgent::Client::HTTPError,
|
785
|
+
"Could not parse response '#{body}'; the app has probably crashed"
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
# @!visibility private
|
790
|
+
def expect_200_response(response)
|
791
|
+
body = response_body_to_hash(response)
|
792
|
+
if response.status_code < 300 && !body["error"]
|
793
|
+
return body
|
794
|
+
end
|
795
|
+
|
796
|
+
if response.status_code > 300
|
797
|
+
raise RunLoop::DeviceAgent::Client::HTTPError,
|
798
|
+
%Q[Expected status code < 300, found #{response.status_code}.
|
799
|
+
|
800
|
+
Server replied with:
|
801
|
+
|
802
|
+
#{body}
|
803
|
+
|
804
|
+
]
|
805
|
+
else
|
806
|
+
raise RunLoop::DeviceAgent::Client::HTTPError,
|
807
|
+
%Q[Expected JSON response with no error, but found
|
808
|
+
|
809
|
+
#{body["error"]}
|
810
|
+
|
811
|
+
]
|
812
|
+
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
# @!visibility private
|
817
|
+
def normalize_orientation_position(position)
|
818
|
+
if position.is_a?(Symbol)
|
819
|
+
orientation_for_position_symbol(position)
|
820
|
+
elsif position.is_a?(Fixnum)
|
821
|
+
position
|
822
|
+
else
|
823
|
+
raise ArgumentError, %Q[
|
824
|
+
Expected #{position} to be a Symbol or Fixnum but found #{position.class}
|
825
|
+
|
826
|
+
]
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
# @!visibility private
|
831
|
+
def orientation_for_position_symbol(position)
|
832
|
+
symbol = position.to_sym
|
833
|
+
|
834
|
+
case symbol
|
835
|
+
when :down, :bottom
|
836
|
+
return 1
|
837
|
+
when :up, :top
|
838
|
+
return 2
|
839
|
+
when :right
|
840
|
+
return 3
|
841
|
+
when :left
|
842
|
+
return 4
|
843
|
+
else
|
844
|
+
raise ArgumentError, %Q[
|
845
|
+
Could not coerce '#{position}' into a valid orientation.
|
846
|
+
|
847
|
+
Valid values are: :down, :up, :right, :left, :bottom, :top
|
848
|
+
]
|
849
|
+
end
|
850
|
+
end
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|