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.
@@ -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