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