run_loop 2.1.7 → 2.1.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.
@@ -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