calabash 1.2.1 → 1.9.9.pre1

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