calabash-android 0.1.0 → 0.2.0.pre2

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,3 +1,13 @@
1
+ 0.2.0.pre2:
2
+ A HTTP server has been introduced in the test server so that we no
3
+ longer use socket for communication.
4
+
5
+ 0.2.0.pre1:
6
+ The concept of a Device has been introduced. It will handle all setup
7
+ and communication with physical devices or emulators.
8
+ The device class is needed because we want to interact with more than
9
+ one device at the same time from Calabash.
10
+
1
11
  0.1.0:
2
12
  Added support for testing Xamarin Mono for Android apps.
3
13
  Currently only Release builds are supported.
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'fileutils'
4
+ require 'calabash-android/helpers'
5
+
4
6
 
5
7
  require File.join(File.dirname(__FILE__), "calabash-android-helpers")
6
8
  require File.join(File.dirname(__FILE__), "calabash-android-generate")
@@ -14,6 +16,14 @@ require File.join(File.dirname(__FILE__), "calabash-android-submit")
14
16
  @source_dir = File.join(File.dirname(__FILE__), '..', 'features-skeleton')
15
17
  @script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
16
18
 
19
+ def is_apk_file?(file_path)
20
+ file_path.end_with? ".apk" and File.exist? file_path
21
+ end
22
+
23
+ def relative_to_full_path(file_path)
24
+ File.expand_path(File.join(ENV["PWD"], file_path))
25
+ end
26
+
17
27
  if (ARGV.length == 0)
18
28
  print_usage
19
29
  exit 0
@@ -23,10 +33,17 @@ if cmd == 'help'
23
33
  print_help
24
34
  exit 0
25
35
  elsif cmd == 'build'
26
- calabash_build(ARGV)
36
+ puts "Please specify the app you want to test" if (ARGV.empty? or not is_apk_file?(ARGV.first))
37
+ while not ARGV.empty? and is_apk_file?(ARGV.first)
38
+ calabash_build(relative_to_full_path(ARGV.shift))
39
+ end
27
40
  exit 0
28
41
  elsif cmd == 'run'
29
- calabash_run(ARGV)
42
+ if ARGV.empty? or not is_apk_file?(ARGV.first)
43
+ calabash_run(nil, ARGV)
44
+ else
45
+ calabash_run(ARGV.shift)
46
+ end
30
47
  exit 0
31
48
  elsif cmd == 'gen'
32
49
  calabash_scaffold
@@ -1,8 +1,10 @@
1
- def calabash_build(args)
2
- run_setup_if_settings_does_not_exist
1
+ def calabash_build(app)
3
2
 
4
- @settings = JSON.parse(IO.read(".calabash_settings"))
5
- test_server_template_dir = File.join(File.dirname(__FILE__), '..', 'test-server')
3
+
4
+
5
+ keystore = read_keystore_info()
6
+
7
+ test_server_template_dir = File.join(File.dirname(__FILE__), '..', 'test-server')
6
8
 
7
9
  Dir.mktmpdir do |workspace_dir|
8
10
 
@@ -15,14 +17,14 @@ def calabash_build(args)
15
17
  ant_executable,
16
18
  "clean",
17
19
  "package",
18
- "-Dtested.package_name=#{@settings["package_name"]}",
19
- "-Dtested.main_activity=#{@settings["activity_name"]}",
20
- "-Dtested.project.apk=#{@settings["app_path"]}",
21
- "-Dandroid.api.level=#{@settings["api_level"]}",
22
- "-Dkey.store=#{@settings["keystore_location"]}",
23
- "-Dkey.store.password=#{@settings["keystore_password"]}",
24
- "-Dkey.alias=#{@settings["keystore_alias"]}",
25
- "-Dkey.alias.password=#{@settings["keystore_alias_password"]}",
20
+ "-Dtested.package_name=#{package_name(app)}",
21
+ "-Dtested.main_activity=#{main_activity(app)}",
22
+ "-Dtested.project.apk=#{app}",
23
+ "-Dandroid.api.level=#{api_level}",
24
+ "-Dkey.store=#{keystore["keystore_location"]}",
25
+ "-Dkey.store.password=#{keystore["keystore_password"]}",
26
+ "-Dkey.alias=#{keystore["keystore_alias"]}",
27
+ "-Dkey.alias.password=#{keystore["keystore_alias_password"]}",
26
28
  ]
27
29
  STDOUT.sync = true
28
30
  IO.popen(args.join(" ")) do |io|
@@ -35,8 +37,22 @@ def calabash_build(args)
35
37
  }
36
38
 
37
39
  test_apk = File.join(@test_server_dir, "bin", "Test.apk")
38
- FileUtils.cp(test_apk, File.join(@support_dir, "Test.apk"))
39
- puts "Done building the test server. Moved it to features/support/Test.apk"
40
+ test_server_file_name = "#{checksum(app)}.apk"
41
+ FileUtils.cp(test_apk, File.join(@support_dir, test_server_file_name))
42
+ puts "Done building the test server. Moved it to features/support/#{test_server_file_name}"
43
+ end
44
+ end
45
+
46
+ def read_keystore_info
47
+ if File.exist? ".calabash_settings"
48
+ JSON.parse(IO.read(".calabash_settings"))
49
+ else
50
+ {
51
+ "keystore_location" => "#{ENV["HOME"]}/.android/debug.keystore",
52
+ "keystore_password" => "android",
53
+ "keystore_alias" => "androiddebugkey",
54
+ "keystore_alias_password" => "android"
55
+ }
40
56
  end
41
57
  end
42
58
 
@@ -2,7 +2,6 @@
2
2
  def calabash_scaffold
3
3
  if File.exists?(@features_dir)
4
4
  puts "A features directory already exists. Stopping..."
5
- #puts "Run calabash-android update for update instructions."
6
5
  exit 1
7
6
  end
8
7
  msg("Question") do
@@ -43,25 +43,10 @@ def print_help
43
43
  print_usage
44
44
  end
45
45
 
46
-
47
46
  def is_json?(str)
48
47
  str[0..0] == '{'
49
48
  end
50
49
 
51
- def run_build_if_test_server_does_not_exist
52
- unless File.exists?(File.join(@support_dir, "Test.apk"))
53
- puts "Could not find the test server"
54
- puts "Should I run calabash-android build for you?"
55
- puts "Please answer yes (y) or no (n)"
56
- if ['yes', 'y'].include? STDIN.gets.chomp
57
- calabash_build([])
58
- else
59
- puts "Please run: calabash-android build"
60
- exit 1
61
- end
62
- end
63
- end
64
-
65
50
  def run_setup_if_settings_does_not_exist
66
51
  unless File.exists?(".calabash_settings")
67
52
  puts "Could not find .calabash_settings."
@@ -1,5 +1,4 @@
1
- def calabash_run(args)
2
- run_build_if_test_server_does_not_exist
1
+ def calabash_run(app_path)
3
2
 
4
3
  old_runner = "android.test.InstrumentationTestRunner"
5
4
  new_rummer = "sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner"
@@ -13,14 +12,17 @@ def calabash_run(args)
13
12
  exit
14
13
  end
15
14
 
16
- settings = JSON.parse(IO.read(".calabash_settings"))
17
-
18
- env ="PACKAGE_NAME=#{settings["package_name"]} "\
19
- "TEST_PACKAGE_NAME=#{settings["package_name"]}.test "\
20
- "APP_PATH=#{settings["app_path"]} "\
21
- "TEST_APP_PATH=features/support/Test.apk "\
22
- "TEST_SERVER_PORT=34777"
23
-
15
+ if app_path
16
+ test_server_path = "features/support/#{checksum(app_path)}.apk"
17
+ env = "PACKAGE_NAME=#{package_name(app_path)} "\
18
+ "TEST_PACKAGE_NAME=#{package_name(test_server_path)} "\
19
+ "APP_PATH=#{app_path} "\
20
+ "TEST_APP_PATH=#{test_server_path} "\
21
+ "TEST_SERVER_PORT=34777"
22
+ else
23
+ env = ""
24
+ end
25
+
24
26
  STDOUT.sync = true
25
27
  cmd = "cucumber -c #{ARGV.join(" ")} #{env}"
26
28
  puts cmd
@@ -8,8 +8,9 @@ Before do |scenario|
8
8
  feature_name = scenario.feature.name
9
9
  if FeatureNameMemory.feature_name != feature_name
10
10
  log "Is first scenario - reinstalling apps"
11
- uninstall_apps
11
+ uninstall_app(ENV["TEST_APP_PATH"])
12
12
  install_app(ENV["TEST_APP_PATH"])
13
+ uninstall_app(ENV["APP_PATH"])
13
14
  install_app(ENV["APP_PATH"])
14
15
  FeatureNameMemory.feature_name = feature_name
15
16
  end
@@ -3,26 +3,5 @@ require 'calabash-android/management/adb'
3
3
  Before do |scenario|
4
4
 
5
5
  return if scenario.failed? #No need to start the server is anything before this has failed.
6
- cmd = "#{adb_command} shell am instrument -w -e class sh.calaba.instrumentationbackend.InstrumentationBackend #{ENV['TEST_PACKAGE_NAME']}/sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner"
7
- log "Starting test server using:"
8
- log cmd
9
- if is_windows?
10
- system(%Q(start /MIN cmd /C #{cmd}))
11
- else
12
- `#{cmd} 1>&2 &`
13
- end
14
-
15
- sleep 2
16
- begin
17
- connect_to_test_server
18
- log "Connection established"
19
- rescue Exception => e
20
- log "Exception:#{e.backtrace}"
21
- end
22
- end
23
-
24
-
25
-
26
- After do |scenario|
27
- disconnect_from_test_server
6
+ start_test_server_in_background
28
7
  end
@@ -0,0 +1,54 @@
1
+ require 'rexml/document'
2
+ require 'rexml/xpath'
3
+
4
+ include REXML
5
+
6
+ def package_name(app)
7
+ require 'rexml/document'
8
+ require 'rexml/xpath'
9
+
10
+ manifest = Document.new(manifest(app))
11
+ manifest.root.attributes['package']
12
+ end
13
+
14
+ def main_activity(app)
15
+ manifest = Document.new(manifest(app))
16
+ main_activity = manifest.elements["//action[@name='android.intent.action.MAIN']/../.."].attributes['name']
17
+ #Handle situation where main activity is on the form '.class_name'
18
+ if main_activity.start_with? "."
19
+ main_activity = package_name(app) + main_activity
20
+ elsif not main_activity.include? "." #This is undocumentet behaviour but Android seems to accept shorthand naming that does not start with '.'
21
+ main_activity = "#{package_name(app)}.#{main_activity}"
22
+ end
23
+ main_activity
24
+ end
25
+
26
+ def api_level
27
+ api_levels = Dir["#{ENV["ANDROID_HOME"]}/platforms/android-*"].collect{|platform| platform.split("-").last.to_i}.sort
28
+ if api_levels.empty?
29
+ raise "Android SDK not found. Please install one of more using #{ENV["ANDROID_HOME"]}/tools/android"
30
+ end
31
+
32
+ api_levels = api_levels.find_all {|l| l > 7}
33
+ if api_levels.empty?
34
+ raise "Android SDK above 7 not found. Please install one of more using #{ENV["ANDROID_HOME"]}/tools/android"
35
+ end
36
+ api_levels.first
37
+ end
38
+
39
+ def manifest(app)
40
+ require 'tmpdir'
41
+ dir = Dir.mktmpdir
42
+ FileUtils.cp(app, File.join(dir, "app.apk"))
43
+
44
+ system "unzip -d #{dir} #{app} AndroidManifest.xml"
45
+
46
+ `java -jar #{File.dirname(__FILE__)}/lib/AXMLPrinter2.jar #{dir}/AndroidManifest.xml`
47
+ end
48
+
49
+ def checksum(file_path)
50
+ require 'digest/md5'
51
+ Digest::MD5.hexdigest(File.read(file_path))
52
+ end
53
+
54
+
@@ -1,11 +0,0 @@
1
- def adb_command
2
- if is_windows?
3
- %Q("#{ENV["ANDROID_HOME"]}\\platform-tools\\adb.exe" #{ENV["ADB_DEVICE_ARG"]})
4
- else
5
- %Q(#{ENV["ANDROID_HOME"]}/platform-tools/adb #{ENV["ADB_DEVICE_ARG"]})
6
- end
7
- end
8
-
9
- def is_windows?
10
- ENV["OS"] == "Windows_NT"
11
- end
@@ -1,8 +1,10 @@
1
1
  require 'json'
2
+ require 'net/http'
2
3
  require 'rubygems'
3
4
  require 'json'
4
5
  require 'socket'
5
6
  require 'timeout'
7
+ require 'calabash-android/helpers'
6
8
 
7
9
 
8
10
  module Calabash module Android
@@ -15,19 +17,7 @@ module Operations
15
17
  end
16
18
 
17
19
  def take_screenshot
18
- path = ENV["SCREENSHOT_PATH_PREFIX"] || "results"
19
- FileUtils.mkdir_p path unless File.exist? path
20
- filename_prefix = FeatureNameMemory.feature_name.gsub(/\s+/, '_').downcase
21
- begin
22
- Timeout.timeout(30) do
23
- file_name = "#{path}/#{filename_prefix}_#{StepCounter.step_line}.png"
24
- log "Taking screenshoot to #{file_name} from device: #{ENV['ADB_DEVICE_ARG']}"
25
- system("java -jar #{File.dirname(__FILE__)}/lib/screenShotTaker.jar #{file_name} #{ENV['ADB_DEVICE_ARG']}")
26
- log "Screenshot stored in: #{file_name}"
27
- end
28
- rescue Timeout::Error
29
- raise Exception, "take_screenshot timed out"
30
- end
20
+ Device.default_device.take_screenshot
31
21
  end
32
22
 
33
23
  def macro(txt)
@@ -39,31 +29,23 @@ module Operations
39
29
  end
40
30
 
41
31
  def performAction(action, *arguments)
42
- log "Action: #{action} - Params: #{arguments.join(', ')}"
32
+ Device.default_device.perform_action(action, *arguments)
33
+ end
43
34
 
44
- action = {"command" => action, "arguments" => arguments}
35
+ def install_app(app_path)
36
+ Device.default_device.install_app(app_path)
37
+ end
45
38
 
46
- Timeout.timeout(300) do
47
- begin
48
- @@client.send(action.to_json + "\n", 0) #force_encoding('UTF-8') seems to be missing from JRuby
49
- result = @@client.readline
50
- rescue Exception => e
51
- log "Error communicating with test server: #{e}"
52
- raise e
53
- end
54
- log "Result:'" + result.strip + "'"
55
- raise "Empty result from TestServer" if result.chomp.empty?
56
- result = JSON.parse(result)
57
- if not result["success"] then
58
- take_screenshot
59
- raise result["message"].to_s
60
- end
61
- return result
62
- end
63
- rescue Timeout::Error
64
- raise Exception, "Step timed out"
39
+ def uninstall_apps
40
+ Device.default_device.uninstall_app(ENV["TEST_PACKAGE_NAME"])
41
+ Device.default_device.uninstall_app(ENV["PACKAGE_NAME"])
65
42
  end
66
43
 
44
+ def start_test_server_in_background
45
+ Device.default_device.start_test_server_in_background()
46
+ end
47
+
48
+
67
49
  def wait_for(timeout, &block)
68
50
  begin
69
51
  Timeout::timeout(timeout) do
@@ -93,16 +75,6 @@ module Operations
93
75
  raise "Not yet implemented."
94
76
  end
95
77
 
96
-
97
- ##adb
98
- def adb_command
99
- if is_windows?
100
- %Q("#{ENV["ANDROID_HOME"]}\\platform-tools\\adb.exe" #{ENV["ADB_DEVICE_ARG"]})
101
- else
102
- %Q(#{ENV["ANDROID_HOME"]}/platform-tools/adb #{ENV["ADB_DEVICE_ARG"]})
103
- end
104
- end
105
-
106
78
  def is_windows?
107
79
  ENV["OS"] == "Windows_NT"
108
80
  end
@@ -110,41 +82,179 @@ module Operations
110
82
 
111
83
  ### app life cycle
112
84
  def connect_to_test_server
113
- log `#{adb_command} forward tcp:#{ENV["TEST_SERVER_PORT"]} tcp:7101`
114
-
115
- end_time = Time.now + 60
116
- begin
117
- Timeout.timeout(10) do
118
- @@client = TCPSocket.open('127.0.0.1',ENV["TEST_SERVER_PORT"])
119
- @@client.send("Ping!\n",0)
120
- log "Got '#{@@client.readline.strip}' from testserver"
121
- end
122
- rescue Exception => e
123
- log "Got exception:#{e}. Retrying!"
124
- sleep(1)
125
- retry unless Time.now > end_time
126
- end
85
+ puts "Explicit calls to connect_to_test_server should be removed."
86
+ puts "Please take a look in your hooks file for calls to this methods."
87
+ puts "(Hooks are stored in features/support)"
127
88
  end
128
89
 
129
90
  def disconnect_from_test_server
130
- log "Closing connection to test"
131
- @@client.close
91
+ puts "Explicit calls to disconnect_from_test_server should be removed."
92
+ puts "Please take a look in your hooks file for calls to this methods."
93
+ puts "(Hooks are stored in features/support)"
132
94
  end
133
95
 
96
+ class Device
97
+ @@default_device = nil
134
98
 
99
+ def self.default_device
100
+ unless @@default_device
101
+ @@default_device = Device.new(ENV["ADB_DEVICE_ARG"], ENV["TEST_SERVER_PORT"], ENV["APP_PATH"], ENV["TEST_APP_PATH"])
102
+ end
103
+ @@default_device
104
+ end
105
+
106
+ def make_default_device
107
+ @@default_device = self
108
+ end
135
109
 
136
- ##location
137
- def set_gps_coordinates_from_location(location)
138
- require 'geocoder'
139
- results = Geocoder.search(location)
140
- raise Exception, "Got no results for #{location}" if results.empty?
110
+ def initialize(serial, server_port, app_path, test_server_path)
111
+ @serial = serial
112
+ @server_port = server_port
113
+ @app_path = app_path
114
+ @test_server_path = test_server_path
141
115
 
142
- best_result = results.first
143
- set_gps_coordinates(best_result.latitude, best_result.longitude)
144
- end
116
+ puts "#{adb_command} forward tcp:b#{server_port} tcp:7102"
117
+ log `#{adb_command} forward tcp:#{server_port} tcp:7102`
118
+ =begin
119
+ begin
120
+ Timeout::timeout(15) do
121
+ puts http("/ping")
122
+ end
123
+ rescue Timeout::Error
124
+ msg = "Unable to make connection to Calabash Test Server at http://127.0.0.1:#{@server_port}/\n"
125
+ msg << "Please check the logcat output for more info about what happened\n"
126
+ raise msg
127
+ end
128
+ =end
129
+ end
130
+
131
+ def reinstall_apps()
132
+ uninstall_app(package_name(@app_path))
133
+ install_app(@app_path)
134
+ uninstall_app(package_name(@test_server_path))
135
+ install_app(@test_server_path)
136
+ end
137
+
138
+ def install_app(app_path)
139
+ cmd = "#{adb_command} install #{app_path}"
140
+ log "Installing: #{app_path}"
141
+ result = `#{cmd}`
142
+ if result.include? "Success"
143
+ log "Success"
144
+ else
145
+ log "#Failure"
146
+ log "'#{cmd}' said:"
147
+ log result.strip
148
+ raise "Could not install app #{app_path}: #{result.strip}"
149
+ end
150
+ end
145
151
 
146
- def set_gps_coordinates(latitude, longitude)
147
- performAction('set_gps_coordinates', latitude, longitude)
152
+ def uninstall_app(package_name)
153
+ log "Uninstalling: #{package_name}"
154
+ log `#{adb_command} uninstall #{package_name}`
155
+ end
156
+
157
+ def perform_action(action, *arguments)
158
+ log "Action: #{action} - Params: #{arguments.join(', ')}"
159
+
160
+ params = {"command" => action, "arguments" => arguments}
161
+
162
+ Timeout.timeout(300) do
163
+ begin
164
+ result = http("/", params)
165
+ rescue Exception => e
166
+ log "Error communicating with test server: #{e}"
167
+ raise e
168
+ end
169
+ log "Result:'" + result.strip + "'"
170
+ raise "Empty result from TestServer" if result.chomp.empty?
171
+ result = JSON.parse(result)
172
+ if not result["success"] then
173
+ take_screenshot
174
+ raise result["message"].to_s
175
+ end
176
+ return result
177
+ end
178
+ rescue Timeout::Error
179
+ raise Exception, "Step timed out"
180
+ end
181
+
182
+ def http(path, data = {})
183
+ begin
184
+ http = Net::HTTP.new "127.0.0.1", @server_port
185
+ resp, data = http.post(path, "command=#{data.to_json}", {})
186
+ data
187
+ rescue EOFError
188
+ sleep 0.5
189
+ retry
190
+ end
191
+ end
192
+
193
+ def take_screenshot
194
+ path = ENV["SCREENSHOT_PATH_PREFIX"] || "results"
195
+ FileUtils.mkdir_p path unless File.exist? path
196
+ filename_prefix = FeatureNameMemory.feature_name.gsub(/\s+/, '_').downcase
197
+ begin
198
+ Timeout.timeout(30) do
199
+ file_name = "#{path}/#{filename_prefix}_#{StepCounter.step_line}.png"
200
+ log "Taking screenshoot to #{file_name} from device: #{@serial}"
201
+ system("java -jar #{File.dirname(__FILE__)}/lib/screenShotTaker.jar #{file_name} #{device_args}")
202
+ log "Screenshot stored in: #{file_name}"
203
+ end
204
+ rescue Timeout::Error
205
+ raise Exception, "take_screenshot timed out"
206
+ end
207
+ end
208
+
209
+ def adb_command
210
+ if is_windows?
211
+ %Q("#{ENV["ANDROID_HOME"]}\\platform-tools\\adb.exe" #{device_args})
212
+ else
213
+ %Q(#{ENV["ANDROID_HOME"]}/platform-tools/adb #{device_args})
214
+ end
215
+ end
216
+
217
+ def device_args
218
+ if @serial
219
+ "-s #{@serial}"
220
+ else
221
+ ""
222
+ end
223
+ end
224
+
225
+ def is_windows?
226
+ ENV["OS"] == "Windows_NT"
227
+ end
228
+
229
+ def start_test_server_in_background
230
+ test_server_package = package_name(@test_server_path)
231
+ cmd = "#{adb_command} shell am instrument -w -e class sh.calaba.instrumentationbackend.InstrumentationBackend #{test_server_package}/sh.calaba.instrumentationbackend.CalabashInstrumentationTestRunner"
232
+ log "Starting test server using:"
233
+ log cmd
234
+ if is_windows?
235
+ system(%Q(start /MIN cmd /C #{cmd}))
236
+ else
237
+ `#{cmd} 1>&2 &`
238
+ end
239
+ end
240
+
241
+ def log(message)
242
+ $stdout.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{message}" if (ARGV.include? "-v" or ARGV.include? "--verbose")
243
+ end
244
+
245
+ ##location
246
+ def set_gps_coordinates_from_location(location)
247
+ require 'geocoder'
248
+ results = Geocoder.search(location)
249
+ raise Exception, "Got no results for #{location}" if results.empty?
250
+
251
+ best_result = results.first
252
+ set_gps_coordinates(best_result.latitude, best_result.longitude)
253
+ end
254
+
255
+ def set_gps_coordinates(latitude, longitude)
256
+ perform_action('set_gps_coordinates', latitude, longitude)
257
+ end
148
258
  end
149
259
 
150
260
 
@@ -167,7 +277,6 @@ module Operations
167
277
  query(q).map {|e| e['html']}
168
278
  end
169
279
 
170
-
171
280
  def set_text(uiquery, txt)
172
281
  raise "Currently queries are only supported for webviews" unless uiquery.start_with? "webView"
173
282