calabash 1.2.1 → 1.9.9.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +39 -0
  3. data/LICENSE +204 -21
  4. data/README.md +36 -6
  5. data/VERSIONING.md +16 -0
  6. data/bin/calabash +95 -0
  7. data/lib/calabash.rb +185 -1
  8. data/lib/calabash/android.rb +64 -0
  9. data/lib/calabash/android/adb.rb +277 -0
  10. data/lib/calabash/android/application.rb +110 -0
  11. data/lib/calabash/android/build.rb +12 -0
  12. data/lib/calabash/android/build/application.rb +13 -0
  13. data/lib/calabash/android/build/build_error.rb +11 -0
  14. data/lib/calabash/android/build/builder.rb +119 -0
  15. data/lib/calabash/android/build/java_keystore.rb +177 -0
  16. data/lib/calabash/android/build/resigner.rb +56 -0
  17. data/lib/calabash/android/build/test_server.rb +27 -0
  18. data/lib/calabash/android/console_helpers.rb +44 -0
  19. data/lib/calabash/android/cucumber.rb +3 -0
  20. data/lib/calabash/android/device.rb +965 -0
  21. data/lib/calabash/android/environment.rb +470 -0
  22. data/lib/calabash/android/gestures.rb +369 -0
  23. data/lib/calabash/android/interactions.rb +45 -0
  24. data/lib/calabash/android/lib/.irbrc +55 -0
  25. data/lib/calabash/android/lib/AndroidManifest.xml +51 -0
  26. data/lib/calabash/android/lib/TestServer.apk +0 -0
  27. data/lib/calabash/android/lib/calmd5/arm64-v8a/calmd5 +0 -0
  28. data/lib/calabash/android/lib/calmd5/arm64-v8a/calmd5-pie +0 -0
  29. data/lib/calabash/android/lib/calmd5/armeabi-v7a/calmd5 +0 -0
  30. data/lib/calabash/android/lib/calmd5/armeabi-v7a/calmd5-pie +0 -0
  31. data/lib/calabash/android/lib/calmd5/armeabi/calmd5 +0 -0
  32. data/lib/calabash/android/lib/calmd5/armeabi/calmd5-pie +0 -0
  33. data/lib/calabash/android/lib/calmd5/mips/calmd5 +0 -0
  34. data/lib/calabash/android/lib/calmd5/mips/calmd5-pie +0 -0
  35. data/lib/calabash/android/lib/calmd5/mips64/calmd5 +0 -0
  36. data/lib/calabash/android/lib/calmd5/mips64/calmd5-pie +0 -0
  37. data/lib/calabash/android/lib/calmd5/x86/calmd5 +0 -0
  38. data/lib/calabash/android/lib/calmd5/x86/calmd5-pie +0 -0
  39. data/lib/calabash/android/lib/calmd5/x86_64/calmd5 +0 -0
  40. data/lib/calabash/android/lib/calmd5/x86_64/calmd5-pie +0 -0
  41. data/lib/calabash/android/lib/screenshot_taker.jar +0 -0
  42. data/lib/calabash/android/life_cycle.rb +37 -0
  43. data/lib/calabash/android/orientation.rb +30 -0
  44. data/lib/calabash/android/physical_buttons.rb +39 -0
  45. data/lib/calabash/android/screenshot.rb +9 -0
  46. data/lib/calabash/android/scroll.rb +5 -0
  47. data/lib/calabash/android/server.rb +10 -0
  48. data/lib/calabash/android/text.rb +54 -0
  49. data/lib/calabash/application.rb +74 -0
  50. data/lib/calabash/cli.rb +12 -0
  51. data/lib/calabash/cli/build.rb +33 -0
  52. data/lib/calabash/cli/console.rb +90 -0
  53. data/lib/calabash/cli/generate.rb +110 -0
  54. data/lib/calabash/cli/helpers.rb +130 -0
  55. data/lib/calabash/cli/resign.rb +33 -0
  56. data/lib/calabash/cli/run.rb +99 -0
  57. data/lib/calabash/cli/setup_keystore.rb +39 -0
  58. data/lib/calabash/color.rb +32 -0
  59. data/lib/calabash/console_helpers.rb +90 -0
  60. data/lib/calabash/defaults.rb +56 -0
  61. data/lib/calabash/device.rb +401 -0
  62. data/lib/calabash/environment.rb +75 -0
  63. data/lib/calabash/gestures.rb +384 -0
  64. data/lib/calabash/http.rb +8 -0
  65. data/lib/calabash/http/error.rb +15 -0
  66. data/lib/calabash/http/request.rb +42 -0
  67. data/lib/calabash/http/retriable_client.rb +156 -0
  68. data/lib/calabash/interactions.rb +105 -0
  69. data/lib/calabash/ios.rb +37 -0
  70. data/lib/calabash/ios/application.rb +119 -0
  71. data/lib/calabash/ios/conditions.rb +79 -0
  72. data/lib/calabash/ios/console_helpers.rb +72 -0
  73. data/lib/calabash/ios/device.rb +24 -0
  74. data/lib/calabash/ios/device/device_implementation.rb +779 -0
  75. data/lib/calabash/ios/device/gestures_mixin.rb +167 -0
  76. data/lib/calabash/ios/device/keyboard_mixin.rb +133 -0
  77. data/lib/calabash/ios/device/physical_device_mixin.rb +266 -0
  78. data/lib/calabash/ios/device/rotation_mixin.rb +124 -0
  79. data/lib/calabash/ios/device/routes/backdoor_route_mixin.rb +86 -0
  80. data/lib/calabash/ios/device/routes/condition_route_mixin.rb +62 -0
  81. data/lib/calabash/ios/device/routes/error.rb +8 -0
  82. data/lib/calabash/ios/device/routes/handle_route_mixin.rb +102 -0
  83. data/lib/calabash/ios/device/routes/map_route_mixin.rb +38 -0
  84. data/lib/calabash/ios/device/routes/playback_route_mixin.rb +70 -0
  85. data/lib/calabash/ios/device/routes/response_parser.rb +48 -0
  86. data/lib/calabash/ios/device/routes/uia_route_mixin.rb +238 -0
  87. data/lib/calabash/ios/device/runtime_attributes.rb +184 -0
  88. data/lib/calabash/ios/device/status_bar_mixin.rb +17 -0
  89. data/lib/calabash/ios/device/text_mixin.rb +19 -0
  90. data/lib/calabash/ios/device/uia_keyboard_mixin.rb +188 -0
  91. data/lib/calabash/ios/device/uia_mixin.rb +12 -0
  92. data/lib/calabash/ios/environment.rb +41 -0
  93. data/lib/calabash/ios/interactions.rb +10 -0
  94. data/lib/calabash/ios/lib/.irbrc +55 -0
  95. data/lib/calabash/ios/lib/recordings/rotate_left_home_down_ipad.base64 +2 -0
  96. data/lib/calabash/ios/lib/recordings/rotate_left_home_down_iphone.base64 +2 -0
  97. data/lib/calabash/ios/lib/recordings/rotate_left_home_left_ipad.base64 +2 -0
  98. data/lib/calabash/ios/lib/recordings/rotate_left_home_left_iphone.base64 +2 -0
  99. data/lib/calabash/ios/lib/recordings/rotate_left_home_right_ipad.base64 +2 -0
  100. data/lib/calabash/ios/lib/recordings/rotate_left_home_right_iphone.base64 +2 -0
  101. data/lib/calabash/ios/lib/recordings/rotate_left_home_up_ipad.base64 +2 -0
  102. data/lib/calabash/ios/lib/recordings/rotate_left_home_up_iphone.base64 +2 -0
  103. data/lib/calabash/ios/lib/recordings/rotate_right_home_down_ipad.base64 +2 -0
  104. data/lib/calabash/ios/lib/recordings/rotate_right_home_down_iphone.base64 +2 -0
  105. data/lib/calabash/ios/lib/recordings/rotate_right_home_left_ipad.base64 +2 -0
  106. data/lib/calabash/ios/lib/recordings/rotate_right_home_left_iphone.base64 +2 -0
  107. data/lib/calabash/ios/lib/recordings/rotate_right_home_right_ipad.base64 +2 -0
  108. data/lib/calabash/ios/lib/recordings/rotate_right_home_right_iphone.base64 +2 -0
  109. data/lib/calabash/ios/lib/recordings/rotate_right_home_up_ipad.base64 +2 -0
  110. data/lib/calabash/ios/lib/recordings/rotate_right_home_up_iphone.base64 +2 -0
  111. data/lib/calabash/ios/orientation.rb +117 -0
  112. data/lib/calabash/ios/scroll.rb +504 -0
  113. data/lib/calabash/ios/server.rb +73 -0
  114. data/lib/calabash/ios/text.rb +248 -0
  115. data/lib/calabash/ios/uia.rb +24 -0
  116. data/lib/calabash/lib/skeleton/config/cucumber.yml +6 -0
  117. data/lib/calabash/lib/skeleton/features/sample.feature +5 -0
  118. data/lib/calabash/lib/skeleton/features/step_definitions/calabash_steps.rb +29 -0
  119. data/lib/calabash/lib/skeleton/features/support/env.rb +54 -0
  120. data/lib/calabash/lib/skeleton/features/support/hooks.rb +83 -0
  121. data/lib/calabash/life_cycle.rb +111 -0
  122. data/lib/calabash/location.rb +51 -0
  123. data/lib/calabash/logger.rb +87 -0
  124. data/lib/calabash/orientation.rb +84 -0
  125. data/lib/calabash/page.rb +35 -0
  126. data/lib/calabash/patch.rb +14 -0
  127. data/lib/calabash/patch/array.rb +16 -0
  128. data/lib/calabash/patch/run_loop.rb +90 -0
  129. data/lib/calabash/query.rb +160 -0
  130. data/lib/calabash/query_result.rb +85 -0
  131. data/lib/calabash/screenshot.rb +89 -0
  132. data/lib/calabash/server.rb +16 -0
  133. data/lib/calabash/text.rb +76 -0
  134. data/lib/calabash/utility.rb +58 -0
  135. data/lib/calabash/version.rb +3 -1
  136. data/lib/calabash/wait.rb +474 -0
  137. metadata +462 -24
@@ -0,0 +1,3 @@
1
+ require 'calabash/android'
2
+
3
+ World(Calabash::Android)
@@ -0,0 +1,965 @@
1
+ require 'json'
2
+
3
+ module Calabash
4
+ module Android
5
+ # A representation of a Calabash Android device.
6
+ class Device < ::Calabash::Device
7
+ attr_reader :adb
8
+
9
+ def initialize(identifier, server)
10
+ super
11
+ @adb = ADB.new(identifier)
12
+
13
+ http_client.on_error(Errno::ECONNREFUSED) do |server|
14
+ port_forward(server.endpoint.port, server.test_server_port)
15
+ end
16
+ end
17
+
18
+ def self.default_serial
19
+ serials = list_serials
20
+
21
+ if Environment::DEVICE_IDENTIFIER
22
+ index = serials.index(Environment::DEVICE_IDENTIFIER)
23
+
24
+ if index
25
+ serials[index]
26
+ else
27
+ raise "A device with the serial '#{Environment::DEVICE_IDENTIFIER}' is not visible on adb"
28
+ end
29
+ else
30
+ if serials.length == 0
31
+ raise 'No devices visible on adb. Ensure a device is visible in `adb devices`'
32
+ end
33
+
34
+ if serials.length > 1
35
+ raise 'More than one device connected. Use CAL_DEVICE_ID to select serial'
36
+ end
37
+
38
+ serials.first
39
+ end
40
+ end
41
+
42
+ def self.list_serials
43
+ output = ADB.command('devices')
44
+ lines = output.lines
45
+ index = lines.index{|line| line.start_with?('List of devices attached')}
46
+
47
+ if index.nil?
48
+ raise "Could not parse adb output: '#{lines}'"
49
+ end
50
+
51
+ device_lines = lines[(index+1)..-1].select{|line| line.strip != ''}
52
+
53
+ device_lines.collect do |line|
54
+ line.match(/([^\s]+)/).captures.first
55
+ end
56
+ end
57
+
58
+ def installed_packages
59
+ adb.shell('pm list packages').lines.map do |line|
60
+ line.sub('package:', '').chomp
61
+ end
62
+ end
63
+
64
+ def installed_apps
65
+ adb.shell('pm list packages -f').lines.map do |line|
66
+ # line will be package:<path>=<package>
67
+ # e.g. "package:/system/app/GoogleEars.apk=com.google.android.ears"
68
+ info = line.sub("package:", "")
69
+
70
+ app_path, app_id = info.split('=').map(&:chomp)
71
+
72
+ {package: app_id, path: app_path}
73
+ end
74
+ end
75
+
76
+ def test_server_responding?
77
+ begin
78
+ http_client.get(HTTP::Request.new('ping'), retries: 1).body == 'pong'
79
+ rescue HTTP::Error => _
80
+ false
81
+ end
82
+ end
83
+
84
+ def test_server_ready?
85
+ begin
86
+ http_client.get(HTTP::Request.new('ready')).body == 'true'
87
+ rescue HTTP::Error => _
88
+ false
89
+ end
90
+ end
91
+
92
+ def port_forward(host_port, test_server_port = nil)
93
+ if test_server_port.nil?
94
+ test_server_port = server.test_server_port
95
+ end
96
+
97
+ adb_forward_cmd = ['forward', "tcp:#{host_port}", "tcp:#{test_server_port}"]
98
+ adb.command(*adb_forward_cmd)
99
+ end
100
+
101
+ def make_map_parameters(query, map_method_name, *method_args)
102
+ converted_args = []
103
+
104
+ method_args.each do |arg|
105
+ if arg.is_a?(Hash)
106
+ if arg.keys.length > 1
107
+ raise "Cannot map '#{arg}'. More than one key (method name) is not allowed."
108
+ end
109
+
110
+ if arg.keys.length == 0
111
+ raise "Cannot map '#{arg}'. No key (method name) is given."
112
+ end
113
+
114
+ method_name = arg.keys.first.to_s
115
+ value = arg.values.first
116
+
117
+ if value.is_a?(Array)
118
+ arguments = value
119
+ else
120
+ arguments = [value]
121
+ end
122
+
123
+ converted =
124
+ {
125
+ method_name: method_name,
126
+ arguments: arguments
127
+ }
128
+
129
+ converted_args << converted
130
+ elsif arg.is_a?(Symbol)
131
+ method_name = arg.to_s
132
+ converted_args << method_name
133
+ else
134
+ raise "Invalid value for map: '#{arg}' (#{arg.class})"
135
+ end
136
+ end
137
+
138
+ {
139
+ query: query,
140
+ operation: {
141
+ method_name: map_method_name,
142
+ arguments: converted_args
143
+ }
144
+ }
145
+ end
146
+
147
+ # @!visibility private
148
+ def map_route(query, method_name, *method_args)
149
+ parameters = make_map_parameters(query, method_name, *method_args)
150
+
151
+ request = HTTP::Request.new('map', params_for_request(parameters))
152
+
153
+ result = JSON.parse(http_client.get(request).body)
154
+
155
+ if result['outcome'] != 'SUCCESS'
156
+ raise "mapping \"#{query}\" with \"#{method_name}\" failed because: #{result['reason']}\n#{result['details']}"
157
+ end
158
+
159
+ result['results']
160
+ end
161
+
162
+ def perform_action(action, *arguments)
163
+ @logger.log "Action: #{action} - Arguments: #{arguments.join(', ')}"
164
+
165
+ parameters = {command: action, arguments: arguments}
166
+ request = HTTP::Request.new('/', params_for_request(parameters))
167
+
168
+ result = JSON.parse(http_client.get(request).body)
169
+
170
+ unless result['success']
171
+ message = result['message'] || result['bonusInformation']
172
+
173
+ if message.is_a?(Array)
174
+ message = message.join("\n")
175
+ end
176
+
177
+ if message.nil?
178
+ raise "Could not perform action '#{action}'"
179
+ else
180
+ raise message
181
+ end
182
+ end
183
+
184
+ result
185
+ end
186
+
187
+ def enter_text(text)
188
+ perform_action('keyboard_enter_text', text)
189
+ end
190
+
191
+ def md5_checksum(file_path)
192
+ result = adb.shell("#{md5_binary} '#{file_path}'")
193
+ captures = result.match(/(\w+)/).captures
194
+
195
+ if captures.length != 1
196
+ raise "Invalid MD5 result '#{result}' using #{md5_binary}"
197
+ end
198
+
199
+ captures[0]
200
+ end
201
+
202
+ # @!visibility private
203
+ def backdoor(method, *arguments)
204
+ parameters = {method_name: method, arguments: arguments}
205
+ json = parameters.to_json
206
+ request = HTTP::Request.new('/backdoor', json: json)
207
+
208
+ body = http_client.get(request).body
209
+ result = JSON.parse(body)
210
+
211
+ if result['outcome'] != 'SUCCESS'
212
+ details = if result['detail'].nil? || result['detail'].empty?
213
+ ''
214
+ else
215
+ "\n#{result['detail']}"
216
+ end
217
+
218
+ raise "backdoor #{parameters} failed because: #{result['result']}#{details}"
219
+ end
220
+
221
+ result['result']
222
+ end
223
+
224
+ def go_home
225
+ adb.shell("input keyevent 3")
226
+ end
227
+
228
+ def set_location(location)
229
+ perform_action('set_gps_coordinates',
230
+ location[:latitude], location[:longitude])
231
+ end
232
+
233
+ def resume_app(path_or_application)
234
+ application = parse_path_or_app_parameters(path_or_application)
235
+
236
+ if app_running?(application)
237
+ main_activity = nil
238
+
239
+ begin
240
+ main_activity = application.main_activity
241
+ rescue
242
+ raise 'Could not detect a launchable activity. This is needed to resume the app'
243
+ end
244
+
245
+ resume_activity(application.identifier, main_activity)
246
+ else
247
+ raise "The app '#{application.identifier}' is not running"
248
+ end
249
+
250
+ true
251
+ end
252
+
253
+ def resume_activity(package, activity)
254
+ if package_running?(package)
255
+ if info[:sdk_version] >= 11
256
+ begin
257
+ perform_action('resume_application', package)
258
+ rescue EnsureInstrumentActionError => e
259
+ raise "Failed to resume app: #{e.message}"
260
+ end
261
+ else
262
+ adb.shell("am start -n '#{package}/#{activity}'")
263
+ end
264
+ else
265
+ raise "The app '#{package}' is not running"
266
+ end
267
+ end
268
+
269
+ def app_running?(path_or_application)
270
+ application = parse_path_or_app_parameters(path_or_application)
271
+
272
+ package_running?(application.identifier)
273
+ end
274
+
275
+ def current_focus
276
+ # Example: mFocusedApp=AppWindowToken{42c52610 token=Token{42b5d048 ActivityRecord{42a7bcc8 u0 com.example/.MainActivity t3}}}
277
+ result = adb.shell('dumpsys window windows')
278
+
279
+ grep_words = ['mCurrentFocus', 'mFocusedApp']
280
+
281
+ grep_words.each do |grep_word|
282
+ result.lines.reverse.each do |line|
283
+ match = line.match(/#{grep_word}=.*\{[\w]+\s*([\w\.\:\!]+\s*)*\/*([\w\.]+)*/)
284
+
285
+ if match && match.captures.length == 2 && !match.captures.any?(&:nil?)
286
+ captures = match.captures
287
+ package = captures[0]
288
+ activity_simple_name = captures[1]
289
+
290
+ activity = if activity_simple_name.start_with?('.')
291
+ "#{package}#{activity_simple_name}"
292
+ else
293
+ activity_simple_name
294
+ end
295
+
296
+ return {activity: activity, package: package}
297
+ end
298
+ end
299
+ end
300
+
301
+ raise "Unexpected output from `dumpsys window windows`"
302
+ end
303
+
304
+ def evaluate_javascript_in(query, javascript)
305
+ parameters =
306
+ {
307
+ query: Query.new(query),
308
+ operation: {method_name: 'execute-javascript'},
309
+ javascript: javascript
310
+ }
311
+
312
+ json = parameters.to_json
313
+ request = HTTP::Request.new('/map', json: json)
314
+
315
+ body = http_client.get(request).body
316
+ result = JSON.parse(body)
317
+
318
+ if result['outcome'] != 'SUCCESS'
319
+ if result['results']
320
+ parsed_result = result['results'].map {|r| "\"#{r}\","}.join("\n")
321
+
322
+ raise "Could not evaluate javascript: \n#{parsed_result}"
323
+ else
324
+ raise "Could not evaluate javascript: \n#{result['detail']}"
325
+ end
326
+ end
327
+
328
+ Calabash::QueryResult.create(result['results'], query)
329
+ end
330
+
331
+ private
332
+
333
+ def package_running?(package)
334
+ running_packages.include?(package)
335
+ end
336
+
337
+ def running_packages
338
+ ps.lines.map(&:split).map(&:last)
339
+ end
340
+
341
+ def ps
342
+ adb.shell('ps')
343
+ end
344
+
345
+ def calabash_server_failure_file_path(application)
346
+ "/data/data/#{application.test_server.identifier}/files/calabash_failure.out"
347
+ end
348
+
349
+ def calabash_server_finished_file_path(application)
350
+ "/data/data/#{application.test_server.identifier}/files/calabash_finished.out"
351
+ end
352
+
353
+ def calabash_server_failure_exists?(application)
354
+ cmd = "ls #{calabash_server_failure_file_path(application)}"
355
+
356
+ adb.shell(cmd, no_exit_code_check: true).chomp ==
357
+ calabash_server_failure_file_path(application)
358
+ end
359
+
360
+ def calabash_server_finished_exists?(application)
361
+ cmd = "ls #{calabash_server_finished_file_path(application)}"
362
+
363
+ adb.shell(cmd, no_exit_code_check: true).chomp ==
364
+ calabash_server_finished_file_path(application)
365
+ end
366
+
367
+ def read_calabash_sever_failure(application)
368
+ adb.shell("cat #{calabash_server_failure_file_path(application)}")
369
+ end
370
+
371
+ def read_calabash_sever_finished(application)
372
+ adb.shell("cat #{calabash_server_finished_file_path(application)}")
373
+ end
374
+
375
+ def clear_calabash_server_report(application)
376
+ if installed_packages.include?(application.test_server.identifier)
377
+ adb.shell("am start -e method clear -n #{application.test_server.identifier}/sh.calaba.instrumentationbackend.StatusReporterActivity")
378
+ end
379
+ end
380
+
381
+ def _start_app(application, options={})
382
+ env_options = {}
383
+
384
+ options.fetch(:extras, {}).each do |k, v|
385
+ env_options[k] = v
386
+ end
387
+
388
+ env_options[:test_server_port] = server.test_server_port
389
+ env_options[:target_package] = application.identifier
390
+
391
+ env_options[:class] = options.fetch(:class, 'sh.calaba.instrumentationbackend.InstrumentationBackend')
392
+
393
+ if options[:activity]
394
+ env_options[:main_activity] = options[:activity]
395
+ end
396
+
397
+ if application.test_server.nil?
398
+ raise 'Invalid application. No test-server set.'
399
+ end
400
+
401
+ unless app_installed?(application.identifier)
402
+ raise "The application '#{application.identifier}' is not installed"
403
+ end
404
+
405
+ unless app_installed?(application.test_server.identifier)
406
+ raise "The test-server '#{application.test_server.identifier}' is not installed"
407
+ end
408
+
409
+ ensure_screen_on
410
+
411
+ # Clear any old error reports
412
+ clear_calabash_server_report(application)
413
+
414
+ # We have to forward the port ourselves, as an old test-server could be
415
+ # running on the old port. If the retriable client was able to
416
+ # determine if the port had been forwarded, we would not need this.
417
+ port_forward(server.endpoint.port, server.test_server_port)
418
+
419
+ # For now, the test-server cannot rebind an existing socket.
420
+ # So we have to stop any running Calabash servers from the client
421
+ # for now.
422
+ if test_server_responding?
423
+ @logger.log("A test-server is already running on port #{server.test_server_port}")
424
+ @logger.log("Trying to stop it")
425
+
426
+ begin
427
+ _stop_app
428
+ rescue => _
429
+ raise 'Failed to stop old running test-server'
430
+ end
431
+ end
432
+
433
+ extras = ''
434
+
435
+ env_options.each_pair do |key, val|
436
+ extras = "#{extras} -e \"#{key.to_s}\" \"#{val.to_s}\""
437
+ end
438
+
439
+ begin
440
+ instrument(application,
441
+ 'sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner',
442
+ extras)
443
+ rescue ADB::ADBCallError => e
444
+ raise "Failed to start the application: '#{e.stderr.lines.first.chomp}'"
445
+ end
446
+
447
+ begin
448
+ Retriable.retriable(tries: 30, interval: 1, timeout: 30, on: RetryError) do
449
+ unless test_server_responding?
450
+ # Read any message the test-server might have
451
+ if calabash_server_failure_exists?(application)
452
+ failure_message = read_calabash_sever_failure(application)
453
+
454
+ raise "Failed to start the application: #{parse_failure_message(failure_message)}"
455
+ end
456
+
457
+ raise RetryError
458
+ end
459
+ end
460
+ rescue RetryError => _
461
+ @logger.log('Could not contact test-server', :error)
462
+ @logger.log('For information, see the adb logcat', :error)
463
+ raise 'Could not contact test-server'
464
+ end
465
+
466
+ begin
467
+ Retriable.retriable(tries: 10, interval: 1, timeout: 10) do
468
+ unless test_server_ready?
469
+ raise RetryError
470
+ end
471
+ end
472
+ rescue RetryError => _
473
+ @logger.log('Test-server was never ready', :error)
474
+ @logger.log('For information, see the adb logcat', :error)
475
+ raise 'Test-server was never ready'
476
+ end
477
+
478
+ # Return true to avoid cluttering the console
479
+ true
480
+ end
481
+
482
+ # @!visibility private
483
+ def _stop_app
484
+ Retriable.retriable(tries: 5, interval: 1) do
485
+ begin
486
+ http_client.get(HTTP::Request.new('kill'), retries: 1, interval: 0)
487
+ rescue HTTP::Error => _
488
+ # It's fine that we can't contact the test-server, as it might already have been shut down
489
+ if test_server_responding?
490
+ raise 'Could not kill the test-server'
491
+ end
492
+ end
493
+ end
494
+
495
+ # Return true to avoid cluttering the console
496
+ true
497
+ end
498
+
499
+ def parse_failure_message(message)
500
+ case message
501
+ when 'E_NO_LAUNCH_INTENT_FOR_PACKAGE'
502
+ 'The application does not have an default launchable activity. Specify :activity in #start_app'
503
+ when 'E_COULD_NOT_DETECT_MAIN_ACTIVITY'
504
+ 'Could not detect the main activity of the application. Specify :activity in #start_app'
505
+ when 'E_NO_INTERNET_PERMISSION'
506
+ 'The application does not have internet permission. Add the internet permission to your manifest'
507
+ else
508
+ message
509
+ end
510
+ end
511
+
512
+ # @!visibility private
513
+ def ensure_screen_on
514
+ unless screen_on?
515
+ # Tap the 'lock' button
516
+ Logger.info "Screen is off, turning screen on."
517
+ adb.shell('input keyevent 26')
518
+ end
519
+
520
+ time_start = Time.now
521
+
522
+ while Time.now - time_start < 5
523
+ return true if screen_on?
524
+ end
525
+
526
+ raise 'Could not turn screen on'
527
+ end
528
+
529
+ # @!visibility private
530
+ def instrument(application, test_server_activity, extras = '')
531
+ unless application.is_a?(Android::Application)
532
+ raise ArgumentError, "Invalid application type '#{application.class}'"
533
+ end
534
+
535
+ if application.test_server.nil?
536
+ raise ArgumentError, "No test server set for '#{application}'"
537
+ end
538
+
539
+ unless app_installed?(application.test_server.identifier)
540
+ raise "The test-server '#{application.test_server.identifier}' is not installed"
541
+ end
542
+
543
+ cmd = "am instrument #{extras} #{application.test_server.identifier}/#{test_server_activity}"
544
+
545
+ @logger.log "Starting '#{test_server_activity}' using: '#{cmd}'"
546
+
547
+ adb.shell(cmd)
548
+ end
549
+
550
+ # @!visibility private
551
+ class EnsureInstrumentActionError < RuntimeError; end
552
+
553
+ # @!visibility private
554
+ def ensure_instrument_action(application, test_server_activity, extras = '')
555
+ clear_calabash_server_report(application)
556
+
557
+ begin
558
+ instrument(application, test_server_activity, extras)
559
+ rescue ADB::ADBCallError => e
560
+ raise EnsureInstrumentActionError, e
561
+ end
562
+
563
+ begin
564
+ Timeout.timeout(10) do
565
+ loop do
566
+ if calabash_server_failure_exists?(application)
567
+ failure_message = read_calabash_sever_failure(application)
568
+
569
+ raise EnsureInstrumentActionError, parse_failure_message(failure_message)
570
+ end
571
+
572
+ if calabash_server_finished_exists?(application)
573
+ output = read_calabash_sever_finished(application)
574
+
575
+ if output == 'SUCCESSFUL'
576
+ break
577
+ end
578
+ end
579
+ end
580
+ end
581
+ rescue Timeout::Error => _
582
+ raise EnsureInstrumentActionError, 'Timed out waiting for status'
583
+ end
584
+ end
585
+
586
+ # @!visibility private
587
+ def ts_clear_app_data(application)
588
+ begin
589
+ ensure_instrument_action(application, 'sh.calaba.instrumentationbackend.ClearAppData2')
590
+ rescue EnsureInstrumentActionError => e
591
+ raise "Failed to clear app data: #{e.message}"
592
+ end
593
+ end
594
+
595
+ # @!visibility private
596
+ def screen_on?
597
+ # Lollipop removed this output
598
+ if info[:sdk_version] < 20
599
+ results = adb.shell('dumpsys input_method')
600
+ output = results.lines.grep(/mScreenOn=(\w+)/)
601
+
602
+ if output.empty?
603
+ raise "Could not find 'mScreenOn'"
604
+ end
605
+
606
+ parsed_result = output.first.match(/mScreenOn=(\w+)/)
607
+ parsed_result.captures.first == 'true'
608
+ else
609
+ results = adb.shell('dumpsys power')
610
+ output = results.lines.grep(/mWakefulness=(\w+)/)
611
+
612
+ if output.empty?
613
+ raise "Could not find 'mWakefulness'"
614
+ end
615
+
616
+ parsed_result = output.first.match(/mWakefulness=(\w+)/)
617
+ parsed_result.captures.first == 'Awake'
618
+ end
619
+ end
620
+
621
+ # @!visibility private
622
+ def app_installed?(identifier)
623
+ installed_packages.include?(identifier)
624
+ end
625
+
626
+ # @!visibility private
627
+ def _screenshot(path)
628
+ cmd = "java -jar \"#{Screenshot::SCREENSHOT_JAR_PATH}\" #{identifier} \"#{File.expand_path(path)}\""
629
+
630
+ @logger.log "Taking screenshot using '#{cmd}'"
631
+ raise 'Could not take screenshot' unless system(cmd)
632
+
633
+ @logger.log("Saved screenshot as #{File.expand_path(path)}", :info)
634
+ path
635
+ end
636
+
637
+ # @!visibility private
638
+ def _clear_app_data(application)
639
+ ts_clear_app_data(application)
640
+
641
+ # Return true to avoid cluttering the console
642
+ true
643
+ end
644
+
645
+ # @!visibility private
646
+ def _install_app(application)
647
+ @logger.log "About to install #{application.path}"
648
+
649
+ if installed_packages.include?(application.identifier)
650
+ @logger.log 'Application is already installed. Uninstalling application.'
651
+ _uninstall_app(application)
652
+ end
653
+
654
+ adb_install_app(application)
655
+
656
+ if application.is_a?(Android::Application)
657
+ if application.test_server
658
+ @logger.log 'Installing the test-server as well'
659
+ install_app(application.test_server)
660
+ end
661
+ end
662
+
663
+ # Return true to avoid cluttering the console
664
+ true
665
+ end
666
+
667
+ # @!visibility private
668
+ def _ensure_app_installed(application)
669
+ @logger.log "Ensuring #{application.path} is installed"
670
+
671
+ if installed_packages.include?(application.identifier)
672
+ @logger.log 'Application is already installed. Ensuring right checksum'
673
+
674
+ installed_app = installed_apps.find{|app| app[:package] == application.identifier}
675
+ installed_app_md5_checksum = md5_checksum(installed_app[:path])
676
+
677
+ if application.md5_checksum != installed_app_md5_checksum
678
+ @logger.log("The md5 checksum has changed (#{application.md5_checksum} != #{installed_app_md5_checksum}.", :info)
679
+ _install_app(application)
680
+ end
681
+ else
682
+ adb_install_app(application)
683
+ end
684
+
685
+ if application.is_a?(Android::Application)
686
+ if application.test_server
687
+ @logger.log 'Ensuring the test-server is installed as well'
688
+ ensure_app_installed(application.test_server)
689
+ end
690
+ end
691
+
692
+ # Return true to avoid cluttering the console
693
+ true
694
+ end
695
+
696
+ # @!visibility private
697
+ def _uninstall_app(application)
698
+ if installed_packages.include?(application.identifier)
699
+ adb_uninstall_app(application.identifier)
700
+ end
701
+
702
+ if application.is_a?(Android::Application)
703
+ if application.test_server
704
+ @logger.log 'Uninstalling the test-server as well'
705
+ uninstall_app(application.test_server)
706
+ end
707
+ end
708
+
709
+ # Return true to avoid cluttering the console
710
+ true
711
+ end
712
+
713
+ # @!visibility private
714
+ def _tap(query, options={})
715
+ x = options[:at][:x]
716
+ y = options[:at][:y]
717
+ offset = options[:offset]
718
+
719
+ gesture_options =
720
+ {
721
+ x: x,
722
+ y: y,
723
+ offset: offset,
724
+ }
725
+
726
+ gesture = Gestures::Gesture.tap(gesture_options)
727
+
728
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
729
+ query: query,
730
+ timeout: options[:timeout]))
731
+ end
732
+
733
+ # @!visibility private
734
+ def _double_tap(query, options={})
735
+ x = options[:at][:x]
736
+ y = options[:at][:y]
737
+ offset = options[:offset]
738
+
739
+ gesture_options =
740
+ {
741
+ x: x,
742
+ y: y,
743
+ offset: offset,
744
+ }
745
+
746
+ gesture = Gestures::Gesture.double_tap(gesture_options)
747
+
748
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
749
+ query_string: query.to_s,
750
+ timeout: options[:timeout]))
751
+ end
752
+
753
+ # @!visibility private
754
+ def _long_press(query, options={})
755
+ x = options[:at][:x]
756
+ y = options[:at][:y]
757
+ offset = options[:offset]
758
+ duration = options[:duration]
759
+
760
+ gesture_options =
761
+ {
762
+ x: x,
763
+ y: y,
764
+ offset: offset,
765
+ time: duration
766
+ }
767
+
768
+ gesture = Gestures::Gesture.tap(gesture_options)
769
+
770
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
771
+ query: query,
772
+ timeout: options[:timeout]))
773
+ end
774
+
775
+ # @!visibility private
776
+ def _pan(query, from, to, options={})
777
+ from_x = from[:x]
778
+ from_y = from[:y]
779
+ from = {x: from_x, y: from_y}
780
+ to_x = to[:x]
781
+ to_y = to[:y]
782
+ to = {x: to_x, y: to_y}
783
+ duration = options[:duration]
784
+
785
+ gesture = Gestures::Gesture.generate_swipe(from, to, time: duration)
786
+
787
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
788
+ query_string: query.to_s,
789
+ timeout: options[:timeout]))
790
+ end
791
+
792
+ # @!visibility private
793
+ def _pan_between(query_from, query_to, options={})
794
+ gesture = Gestures::Gesture.generate_swipe({x: 50, y: 50}, {x: 50, y: 50}, time: options[:duration])
795
+ gesture.gestures.first.touches[0].query = query_from
796
+ gesture.gestures.first.touches[1].query = query_to
797
+
798
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
799
+ query_string: query_to,
800
+ timeout: options[:timeout]))
801
+ end
802
+
803
+ # @!visibility private
804
+ def _flick(query, from, to, options={})
805
+ from_x = from[:x]
806
+ from_y = from[:y]
807
+ from = {x: from_x, y: from_y}
808
+ to_x = to[:x]
809
+ to_y = to[:y]
810
+ to = {x: to_x, y: to_y}
811
+ duration = options[:duration]
812
+
813
+ gesture = Gestures::Gesture.generate_swipe(from, to, time: duration, flick: true)
814
+
815
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
816
+ query_string: query.to_s,
817
+ timeout: options[:timeout]))
818
+ end
819
+
820
+ # @!visibility private
821
+ def _pinch(direction, query, options={})
822
+ gesture = Gestures::Gesture.pinch(direction)
823
+
824
+ execute_gesture(Gestures::Gesture.with_parameters(gesture,
825
+ query_string: query.to_s,
826
+ timeout: options[:timeout]))
827
+ end
828
+
829
+ # @!visibility private
830
+ def adb_uninstall_app(package)
831
+ @logger.log "Uninstalling #{package}"
832
+ result = adb.command('uninstall', package, timeout: 60).lines.last
833
+
834
+ if result.downcase.chomp != 'success'
835
+ raise "Could not uninstall app '#{package}': #{result.chomp}"
836
+ end
837
+
838
+ if installed_packages.include?(package)
839
+ raise "App '#{package}' was not uninstalled"
840
+ end
841
+ end
842
+
843
+ # @!visibility private
844
+ def adb_install_app(application)
845
+ @logger.log "Installing #{application.path}"
846
+ result = adb.command('install' , '-r', application.path, timeout: 60).lines.last
847
+
848
+ if result.downcase.chomp != 'success'
849
+ raise "Could not install app '#{application.identifier}': #{result.chomp}"
850
+ end
851
+
852
+ unless installed_packages.include?(application.identifier)
853
+ raise "App '#{application.identifier}' was not installed"
854
+ end
855
+ end
856
+
857
+ # @!visibility private
858
+ def adb_clear_app_data(package)
859
+ @logger.log "Clearing #{package}"
860
+
861
+ unless installed_packages.include?(package)
862
+ raise "Cannot clear app. '#{package}' is not installed"
863
+ end
864
+
865
+ result = adb.shell("pm clear #{package}").lines.last
866
+
867
+ if result.downcase.chomp != 'success'
868
+ raise "Could not clear app '#{package}': #{result.chomp}"
869
+ end
870
+ end
871
+
872
+ # @!visibility private
873
+ def execute_gesture(multi_touch_gesture)
874
+ request = HTTP::Request.new('gesture', json: multi_touch_gesture.to_json)
875
+
876
+ body = http_client.get(request, timeout: multi_touch_gesture.timeout + 10).body
877
+ result = JSON.parse(body)
878
+
879
+ if result['outcome'] != 'SUCCESS'
880
+ raise "Failed to perform gesture. #{result['reason']}"
881
+ end
882
+
883
+ result = result['results'].first
884
+
885
+ results = []
886
+ queries = multi_touch_gesture.queries
887
+
888
+ result.each do |key, value|
889
+ query = queries.find{|query| query.to_s == key}
890
+ results << QueryResult.create([value], query)
891
+ end
892
+
893
+ if results.length == 1
894
+ results.first
895
+ else
896
+ results
897
+ end
898
+ end
899
+
900
+ # @!visibility private
901
+ def params_for_request(parameters)
902
+ {json: parameters.to_json}
903
+ end
904
+
905
+ # @!visibility private
906
+ def md5_binary
907
+ if @md5_binary
908
+ @md5_binary
909
+ else
910
+ if adb.shell('md5', no_exit_code_check: true).chomp == 'md5 file ...'
911
+ @md5_binary = 'md5'
912
+ else
913
+ # The device does not have 'md5'
914
+ calmd5 = Calabash::Android.binary_location('calmd5', info[:cpu_architecture], can_handle_pie_binaries?)
915
+ adb.command('push', calmd5, '/data/local/tmp/calmd5')
916
+ @md5_binary = '/data/local/tmp/calmd5'
917
+ end
918
+ end
919
+ end
920
+
921
+ # @!visibility private
922
+ def can_handle_pie_binaries?
923
+ # Newer Androids requires PIE enabled executables, older Androids break on them
924
+ info[:sdk_version] >= 16
925
+ end
926
+
927
+ # @!visibility private
928
+ def detect_abi
929
+ abi = adb.shell('getprop ro.product.cpu.abi').chomp
930
+
931
+ if abi == 'armeabi-v7a'
932
+ # armeabi-v7a does not necessarily support NEON vector instructions,
933
+ # our binaries for this arch requires that, so if CPU does not support
934
+ # NEON fall back to regular armeabi
935
+ cpuinfo = adb.shell('cat /proc/cpuinfo')
936
+
937
+ if cpuinfo.match /Features.*neon.*/
938
+ abi
939
+ else
940
+ 'armeabi'
941
+ end
942
+ else
943
+ abi
944
+ end
945
+ end
946
+
947
+ # @!visibility private
948
+ def info
949
+ @info ||=
950
+ {
951
+ os_version: adb.shell('getprop ro.build.version.release').chomp,
952
+ sdk_version: adb.shell('getprop ro.build.version.sdk').to_i,
953
+ product_name: adb.shell('getprop ro.product.name').chomp,
954
+ model: adb.shell('getprop ro.product.model').chomp,
955
+ cpu_architecture: detect_abi,
956
+ manufacturer: adb.shell('getprop ro.product.manufacturer').chomp
957
+ }
958
+ end
959
+
960
+ def world_module
961
+ Calabash::Android
962
+ end
963
+ end
964
+ end
965
+ end