brazenhead 0.1

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