brazenhead 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/driver/AndroidManifest.xml +21 -0
  2. data/driver/brazenhead-release-unsigned.apk +0 -0
  3. data/features/brazenhead.feature +35 -0
  4. data/features/exception.feature +6 -0
  5. data/features/robotium.feature +31 -0
  6. data/features/step_definitions/brazenhead_steps.rb +52 -0
  7. data/features/step_definitions/exception_steps.rb +13 -0
  8. data/features/step_definitions/robotium_steps.rb +62 -0
  9. data/features/support/ApiDemos.apk +0 -0
  10. data/features/support/debug.keystore +0 -0
  11. data/features/support/env.rb +30 -0
  12. data/lib/brazenhead/android.rb +37 -0
  13. data/lib/brazenhead/builder.rb +92 -0
  14. data/lib/brazenhead/call_accumulator.rb +28 -0
  15. data/lib/brazenhead/core_ext/string.rb +7 -0
  16. data/lib/brazenhead/device.rb +35 -0
  17. data/lib/brazenhead/manifest_info.rb +51 -0
  18. data/lib/brazenhead/package.rb +36 -0
  19. data/lib/brazenhead/process.rb +35 -0
  20. data/lib/brazenhead/request.rb +40 -0
  21. data/lib/brazenhead/server.rb +54 -0
  22. data/lib/brazenhead/signer.rb +66 -0
  23. data/lib/brazenhead/version.rb +3 -0
  24. data/lib/brazenhead.rb +42 -0
  25. data/spec/lib/brazenhead/android_spec.rb +35 -0
  26. data/spec/lib/brazenhead/builder_spec.rb +108 -0
  27. data/spec/lib/brazenhead/call_accumulator_spec.rb +11 -0
  28. data/spec/lib/brazenhead/device_spec.rb +27 -0
  29. data/spec/lib/brazenhead/manifest_info_spec.rb +61 -0
  30. data/spec/lib/brazenhead/package_spec.rb +30 -0
  31. data/spec/lib/brazenhead/process_spec.rb +64 -0
  32. data/spec/lib/brazenhead/request_spec.rb +51 -0
  33. data/spec/lib/brazenhead/server_spec.rb +75 -0
  34. data/spec/lib/brazenhead/signer_spec.rb +35 -0
  35. data/spec/lib/brazenhead_spec.rb +34 -0
  36. data/spec/spec_helper.rb +18 -0
  37. metadata +176 -0
@@ -0,0 +1,21 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.leandog.brazenhead"
3
+ android:versionCode="1"
4
+ android:versionName="1.0" >
5
+
6
+ <instrumentation
7
+ android:name="com.leandog.brazenhead.BrazenheadInstrumentation"
8
+ android:targetPackage="com.example.android.apis" />
9
+
10
+ <uses-permission android:name="android.permission.INTERNET" />
11
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
12
+
13
+ <uses-sdk
14
+ android:minSdkVersion="10"
15
+ android:targetSdkVersion="15" />
16
+
17
+ <application android:label="brazenhead">
18
+ <uses-library android:name="android.test.runner"/>
19
+ </application>
20
+
21
+ </manifest>
@@ -0,0 +1,35 @@
1
+ Feature: Functionality provided by the Brazenhead module
2
+
3
+ Scenario: Calling a method which takes no parameters
4
+ When I call the method "scroll_down" on the Brazenhead module
5
+ Then I should receive a successful result from the Brazenhead module
6
+
7
+ Scenario: Calling a method which takes a parameter
8
+ When I call the method "click_on_text" on the Brazenhead module passing "Content"
9
+ Then I should receive a successful result from the Brazenhead module
10
+
11
+ Scenario: Chaining two method calls together
12
+ When I chain together the methods "scroll_down" and "scroll_up" using the target "Robotium"
13
+ Then I should receive a successful result from the Brazenhead module
14
+
15
+ Scenario: Chaining two method calls together with second call on result of first
16
+ When I chain together the methods "getCurrentListViews" and "size" using the target "LastResultOrRobotium"
17
+ Then I should receive a successful result from the Brazenhead module
18
+ And the result from the chained calls should be "1"
19
+
20
+ Scenario: Chaining two method calls should use "LastResultOrRobotium" target by default
21
+ When I chain together the methods "getCurrentListViews" and "size" on the Brazenhead module
22
+ Then I should receive a successful result from the Brazenhead module
23
+ And the result from the chained calls should be "1"
24
+
25
+ Scenario: Chaining two method calls using a variable
26
+ When I call "get_text" passing the argument "Graphics" and saving it into the variable "@@graphics@@"
27
+ And then I call "click_on_view" using teh variable "@@graphics@@" using the target "Robotium"
28
+ Then I should see "AlphaBitmap" from teh Brazenhead module
29
+
30
+ Scenario: Chaining twice
31
+ When I chain together the methods "getCurrentListViews" and "size" on the Brazenhead module
32
+ And I call "get_text" passing the argument "Graphics" and saving it into the variable "@@graphics@@"
33
+ And then I call "click_on_view" using teh variable "@@graphics@@" using the target "Robotium" on the same driver
34
+ Then I should see "AlphaBitmap" from teh Brazenhead module
35
+
@@ -0,0 +1,6 @@
1
+ Feature: Relaying error information back to the client
2
+
3
+ Scenario: Gracefully handing invalid commands
4
+ When I attempt to call a method that does not exist
5
+ Then I should receive an internal server error
6
+ And the exception should have detailed information
@@ -0,0 +1,31 @@
1
+ Feature: Calling Robotium methods
2
+
3
+ Scenario: Getting a result from a parameterless method
4
+ When I do nothing but "scrollDown"
5
+ Then I should receive a successful result
6
+
7
+ Scenario: Calling a method with integers
8
+ When I call a method with an integer
9
+ Then I should receive a successful result
10
+
11
+ Scenario: Calling a method with strings
12
+ When I call a method with a string
13
+ Then I should receive a successful result
14
+
15
+ Scenario: Calling a method with floats
16
+ When I call a method with a float
17
+ Then I should receive a successful result
18
+
19
+ Scenario: Calling a method with booleans
20
+ When I call a method with a boolean
21
+ Then I should receive a successful result
22
+
23
+ Scenario: Getting useful View information
24
+ When I get the first "Text" View I find
25
+ Then I should receive a successful result
26
+ And the view should have some basic information
27
+
28
+ Scenario: Chaining method calls
29
+ When I call "getCurrentListViews" and then I call "size"
30
+ Then I should receive a successful result
31
+ And the result should be "1"
@@ -0,0 +1,52 @@
1
+ When /^I call the method "(.*?)" on the Brazenhead module$/ do |method_name|
2
+ @driver.send method_name
3
+ end
4
+
5
+ When /^I call the method "(.*?)" on the Brazenhead module passing "(.*?)"$/ do |method_name, argument|
6
+ @driver.send method_name, argument
7
+ end
8
+
9
+ When /^I chain together the methods "(.*?)" and "(.*?)" using the target "(.*?)"$/ do |first_method, second_method, target|
10
+ @driver.chain_calls do |driver|
11
+ driver.send first_method, :target => target
12
+ driver.send second_method, :target => target
13
+ end
14
+ end
15
+
16
+ Then /^I should receive a successful result from the Brazenhead module$/ do
17
+ @driver.last_response.code.should == '200'
18
+ end
19
+
20
+ Then /^the result from the chained calls should be "(.*?)"$/ do |result|
21
+ @driver.last_response.body.should == result
22
+ end
23
+
24
+ When /^I chain together the methods "(.*?)" and "(.*?)" on the Brazenhead module$/ do |first_method, second_method|
25
+ @driver.chain_calls do |driver|
26
+ driver.send first_method
27
+ driver.send second_method
28
+ end
29
+ end
30
+
31
+ When /^I call "(.*?)" passing the argument "(.*?)" and saving it into the variable "(.*?)"$/ do |method, argument, variable|
32
+ @first_call = {:name => method, :arguments => argument, :variable => variable}
33
+ end
34
+
35
+ When /^then I call "(.*?)" using teh variable "(.*?)" using the target "(.*?)"$/ do |method, argument, target|
36
+ @driver.chain_calls do |driver|
37
+ driver.send @first_call[:name], @first_call[:arguments], {:variable => @first_call[:variable]}
38
+ driver.send method, argument, {:target => target}
39
+ end
40
+ end
41
+
42
+ When /^then I call "(.*?)" using teh variable "(.*?)" using the target "(.*?)" on the same driver$/ do |method, argument, target|
43
+ @driver.chain_calls do |driver|
44
+ driver.send @first_call[:name], @first_call[:arguments], {:variable => @first_call[:variable]}
45
+ driver.send method, argument, {:target => target}
46
+ end
47
+ end
48
+
49
+ Then /^I should see "(.*?)" from teh Brazenhead module$/ do |value|
50
+ @driver.search_text value
51
+ @driver.last_response.body.should == 'true'
52
+ end
@@ -0,0 +1,13 @@
1
+ When /^I attempt to call a method that does not exist$/ do
2
+ @driver.should_not_exist_at_all
3
+ end
4
+
5
+ Then /^I should receive an internal server error$/ do
6
+ @driver.last_response.code.should eq '500'
7
+ end
8
+
9
+ Then /^the exception should have detailed information$/ do
10
+ json = JSON.parse(@driver.last_response.body)
11
+ json["exception"].should match ".*CommandNotFoundException"
12
+ json["errorMessage"].should match "The \\w+\\(.*\\) method was not found on .*Solo\\."
13
+ end
@@ -0,0 +1,62 @@
1
+ When /^I do nothing but "(.*?)"$/ do |method|
2
+ @driver.send method
3
+ end
4
+
5
+ Then /^I should receive "(.*?)"$/ do |json|
6
+ @driver.last_response.body.should match json
7
+ end
8
+
9
+ When /^I call a method with an integer$/ do
10
+ @driver.scroll_up_list(0)
11
+ end
12
+
13
+ When /^I call a method with a string$/ do
14
+ @driver.search_button('will not find')
15
+ end
16
+
17
+ When /^I call a method with a float$/ do
18
+ @driver.click_on_screen(100.0, 100.0)
19
+ end
20
+
21
+ When /^I call a method with a boolean$/ do
22
+ @driver.search_text('Views', true)
23
+ end
24
+
25
+ Then /^I should receive a successful result$/ do
26
+ @driver.last_response.code.should eq '200'
27
+ end
28
+
29
+ When /^I get the first "(.*?)" View I find$/ do |view_type|
30
+ @driver.send "get_#{view_type.downcase}", 0
31
+ end
32
+
33
+ Then /^the view should have some basic information$/ do
34
+ response = JSON.parse(@driver.last_response.body)
35
+ response.should have_key "id"
36
+ response.should have_key "classType"
37
+ response.should have_key "width"
38
+ response.should have_key "height"
39
+ response.should have_key "screenLocation"
40
+ response.should have_key "windowLocation"
41
+ response.should have_key "left"
42
+ response.should have_key "top"
43
+ response.should have_key "right"
44
+ response.should have_key "bottom"
45
+ end
46
+
47
+ When /^I call "(.*?)" and then I call "(.*?)"$/ do |first_method, next_method|
48
+ @driver.chain_calls do |driver|
49
+ driver.send first_method
50
+ driver.send next_method
51
+ end
52
+ end
53
+
54
+ Then /^the result should be "(.*?)"$/ do |result|
55
+ @driver.last_response.body.should eq result
56
+ end
57
+
58
+ Then /^I should see "(.*?)"$/ do |text|
59
+ @driver.search_text text
60
+ @last_response.body.should eq 'true'
61
+ end
62
+
Binary file
Binary file
@@ -0,0 +1,30 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '../../', 'lib'))
2
+
3
+ require 'brazenhead'
4
+ require 'brazenhead/server'
5
+ require 'ADB'
6
+ require 'childprocess'
7
+
8
+ World(ADB)
9
+
10
+ keystore = {
11
+ :path => 'features/support/debug.keystore',
12
+ :alias => 'androiddebugkey',
13
+ :password => 'android',
14
+ :keystore_password => 'android'
15
+ }
16
+
17
+ server = Brazenhead::Server.new('features/support/ApiDemos.apk', keystore)
18
+
19
+ class Driver
20
+ include Brazenhead
21
+ end
22
+
23
+ Before do
24
+ @driver = Driver.new
25
+ server.start("ApiDemos")
26
+ end
27
+
28
+ After do
29
+ server.stop
30
+ end
@@ -0,0 +1,37 @@
1
+ module Brazenhead
2
+ module Android
3
+ def path_to(sdk)
4
+ jar = android_jar(sdk)
5
+ android_jar_missing(sdk) unless File.exists? jar
6
+ jar
7
+ end
8
+
9
+ def default_keystore
10
+ {:path => default_key_path,
11
+ :alias => 'androiddebugkey',
12
+ :password => 'android',
13
+ :keystore_password => 'android'}
14
+ end
15
+
16
+ private
17
+ def android_home
18
+ ENV['ANDROID_HOME']
19
+ end
20
+
21
+ def platform(sdk)
22
+ "platforms/android-#{sdk}"
23
+ end
24
+
25
+ def default_key_path
26
+ File.expand_path("~/.android/debug.keystore")
27
+ end
28
+
29
+ def android_jar(sdk)
30
+ File.join(android_home, platform(sdk), 'android.jar')
31
+ end
32
+
33
+ def android_jar_missing(sdk)
34
+ raise Exception, "the path to android-#{sdk} was not found"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,92 @@
1
+ require 'tempfile'
2
+ require 'brazenhead/manifest_info'
3
+ require 'brazenhead/package'
4
+ require 'ADB'
5
+
6
+ module Brazenhead
7
+ class Builder
8
+ include Brazenhead::Package
9
+ include ADB
10
+
11
+ def build_for(apk, keystore)
12
+ @source_apk = apk
13
+ @keystore = keystore
14
+ invalid_package_err(apk) unless File.exists? @source_apk
15
+ install_server
16
+ manifest_info
17
+ end
18
+
19
+ private
20
+ def install_server
21
+ Dir.mktmpdir do |dir|
22
+ copy_base_files_to(dir)
23
+ update_manifest_in(dir)
24
+ sign(test_apk_in(dir), @keystore)
25
+ reinstall test_apk_in(dir)
26
+ reinstall @source_apk
27
+ end
28
+ end
29
+
30
+ def reinstall(apk, timeout=90)
31
+ install apk, "-r", {}, timeout
32
+ end
33
+
34
+ def copy_base_files_to(dir)
35
+ [test_apk, manifest].each do |file|
36
+ FileUtils.copy_file(driver_path_for(file), join(dir, file))
37
+ end
38
+ end
39
+
40
+ def driver_path_for(file)
41
+ join(File.expand_path("../../../", __FILE__), 'driver', file)
42
+ end
43
+
44
+ def join(*paths)
45
+ File.join(*paths)
46
+ end
47
+
48
+ def update_manifest_in(dir)
49
+ manifest_path = join(dir, manifest)
50
+
51
+ replace(manifest_path, target_match, target_replace)
52
+ update_manifest(test_apk_in(dir), manifest_path, manifest_info.target_sdk)
53
+ end
54
+
55
+ def test_apk_in(dir)
56
+ join(dir, test_apk)
57
+ end
58
+
59
+ def replace(file, match, replacement)
60
+ File.write(file, File.read(file).gsub(match, replacement))
61
+ end
62
+
63
+ def target_match
64
+ /\btargetPackage="[^"]+"/
65
+ end
66
+
67
+ def target_replace
68
+ "targetPackage=\"#{the_target}\""
69
+ end
70
+
71
+ def the_target
72
+ @the_target ||= manifest_info.package
73
+ end
74
+
75
+ def manifest_info
76
+ @info ||= Brazenhead::ManifestInfo.new(@source_apk)
77
+ end
78
+
79
+ def test_apk
80
+ 'brazenhead-release-unsigned.apk'
81
+ end
82
+
83
+ def manifest
84
+ 'AndroidManifest.xml'
85
+ end
86
+
87
+ def invalid_package_err(apk)
88
+ raise Exception.new("Invalid package path: #{apk}")
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), '/', 'request')
2
+
3
+ module Brazenhead
4
+ class CallAccumulator
5
+ def method_missing(method, *args)
6
+ calls << request.call(method.to_s.to_java_call, args)
7
+ end
8
+
9
+ def message
10
+ "commands=#{calls.to_json}"
11
+ end
12
+
13
+ def clear
14
+ @calls = []
15
+ end
16
+
17
+ private
18
+
19
+ def calls
20
+ @calls ||= []
21
+ end
22
+
23
+ def request
24
+ @request ||= Brazenhead::Request.new
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ class String
2
+ def to_java_call
3
+ return self if self !~ /_/ && self =~ /[A-Z]+.*/
4
+ camel = self.split('_').map{|e| e.capitalize}.join
5
+ camel.sub(camel[0], camel[0].downcase)
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ require 'net/http'
2
+
3
+ module Brazenhead
4
+ class Device
5
+
6
+ def send(message)
7
+ retries = 0
8
+ begin
9
+ @last_response = http.post '/', message
10
+ rescue
11
+ retries += 1
12
+ sleep 0.5
13
+ retry unless retries == 20
14
+ $stderr.puts "Failed to send the command #{message} #{retries} times..."
15
+ raise
16
+ end
17
+ @last_response
18
+ end
19
+
20
+ def stop
21
+ http.post '/kill', ''
22
+ end
23
+
24
+ def last_response
25
+ @last_response
26
+ end
27
+
28
+ private
29
+
30
+ def http
31
+ @http ||= Net::HTTP.new '127.0.0.1', 7777
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ require 'brazenhead/process'
2
+
3
+ module Brazenhead
4
+ class ManifestInfo
5
+
6
+ def package
7
+ first_capture /\bpackage="([^"]+)"/
8
+ end
9
+
10
+ def min_sdk
11
+ sdk(:min) || 1
12
+ end
13
+
14
+ def max_sdk
15
+ sdk(:max)
16
+ end
17
+
18
+ def target_sdk
19
+ sdk(:target)
20
+ end
21
+
22
+ def initialize(apk)
23
+ @apk = apk
24
+ end
25
+
26
+ private
27
+ def manifest
28
+ @manifest ||= load_manifest
29
+ end
30
+
31
+ def load_manifest
32
+ process.run('aapt', 'dump', 'xmltree', @apk, 'AndroidManifest.xml')
33
+ process.last_stdout
34
+ end
35
+
36
+ def first_capture(regex)
37
+ match = regex.match(manifest)
38
+ match.captures[0] unless match.nil?
39
+ end
40
+
41
+ def sdk(which)
42
+ found = first_capture /android:#{which}SdkVersion.*=\(.*\)0x(\h+)/
43
+ found.hex unless found.nil?
44
+ end
45
+
46
+
47
+ def process
48
+ @process ||= Brazenhead::Process.new
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ require 'brazenhead/android'
2
+ require 'brazenhead/signer'
3
+
4
+ module Brazenhead
5
+ module Package
6
+ include Brazenhead::Android
7
+ include Brazenhead::Signer
8
+
9
+ def update_manifest(apk, manifest, min_sdk = 8)
10
+ process.run(*update, *package(apk), *with(manifest), *using(path_to(min_sdk)))
11
+ end
12
+
13
+ private
14
+ def process
15
+ @process ||= Brazenhead::Process.new
16
+ end
17
+
18
+ def update
19
+ "aapt p -u -f".split
20
+ end
21
+
22
+ def package(apk)
23
+ ["-F", apk]
24
+ end
25
+
26
+ def with(manifest)
27
+ ["-M", manifest]
28
+ end
29
+
30
+ def using(path)
31
+ ["-I", path]
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,35 @@
1
+ require 'childprocess'
2
+ require 'tempfile'
3
+
4
+ module Brazenhead
5
+ class Process
6
+
7
+ attr_accessor :last_stdout, :last_stderr
8
+
9
+ def run(process, *args)
10
+ process = ChildProcess.build(process, *args)
11
+ process.io.stdout, process.io.stderr = std_out_err
12
+ process.start
13
+ process.wait
14
+ @last_stdout = output(process.io.stdout)
15
+ @last_stderr = output(process.io.stderr)
16
+ end
17
+
18
+ def std_out_err
19
+ return ::Tempfile.new("brazenhead-proc-out-#{now}"), ::Tempfile.new("brazenhead-proc-err-#{now}")
20
+ end
21
+
22
+ private
23
+ def output(file)
24
+ file.rewind
25
+ out = file.read
26
+ file.close
27
+ file.unlink
28
+ out
29
+ end
30
+
31
+ def now
32
+ "#{Time.now}".gsub(':', '_')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ require 'json'
2
+
3
+ module Brazenhead
4
+ class Request
5
+ def build(method, args)
6
+ "commands=#{[call(method, args)].to_json}"
7
+ end
8
+
9
+ def call(method, args)
10
+ target = parse_target(args[-1])
11
+ variable = parse_variable(args[-1])
12
+ build_call(method, strip_hash_arg(args), variable, target)
13
+ end
14
+
15
+ private
16
+
17
+ def build_call(method, args, variable, target)
18
+ call = {:name => method} if args.empty?
19
+ call = {:name => method, :arguments => args} unless args.empty?
20
+ call[:variable] = variable if variable
21
+ call[:target] = target if target
22
+ call
23
+ end
24
+
25
+ def parse_target(hsh)
26
+ target = hsh.delete(:target) if hsh.is_a?(Hash)
27
+ return target.nil? ? nil : target
28
+ end
29
+
30
+ def parse_variable(hsh)
31
+ variable = hsh.delete(:variable) if hsh.is_a?(Hash)
32
+ return variable.nil? ? nil : variable
33
+ end
34
+
35
+ def strip_hash_arg(args)
36
+ args.delete_at(-1) if args[-1].is_a?(Hash) and args[-1].empty?
37
+ args
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,54 @@
1
+ require 'brazenhead/builder'
2
+ require 'brazenhead/device'
3
+
4
+ module Brazenhead
5
+ class Server
6
+ include Brazenhead::Package
7
+ include ADB
8
+
9
+ def initialize(apk, keystore = default_keystore)
10
+ @apk = apk
11
+ @keystore = keystore
12
+ end
13
+
14
+ def start(activity)
15
+ build
16
+ instrument(runner, :packageName => their_package, :fullLauncherName => full(activity) , :class => the_test)
17
+ end
18
+
19
+ def stop
20
+ device.stop
21
+ end
22
+
23
+ private
24
+ def build
25
+ forward "tcp:7777", "tcp:54767"
26
+ @manifest_info ||= Brazenhead::Builder.new.build_for(@apk, @keystore)
27
+ end
28
+
29
+ def device
30
+ @device ||= Brazenhead::Device.new
31
+ end
32
+
33
+ def the_test
34
+ "#{leandog}.TheTest"
35
+ end
36
+
37
+ def full(activity)
38
+ "#{their_package}.#{activity}"
39
+ end
40
+
41
+ def their_package
42
+ @manifest_info.package
43
+ end
44
+
45
+ def runner
46
+ "#{leandog}/#{leandog}.BrazenheadInstrumentation"
47
+ end
48
+
49
+ def leandog
50
+ 'com.leandog.brazenhead'
51
+ end
52
+
53
+ end
54
+ end