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