rubotium 0.0.7 → 0.0.17

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72623c02c54541abecf0431da8b66994b1404a95
4
- data.tar.gz: 25d93c5df3c122e59a6fe66f9d848c5be3744556
3
+ metadata.gz: d1b0f0bd231baeeb352f147b7eed2517a2bbf3c0
4
+ data.tar.gz: c7cfcc8c601126f54bc4dfea8c6a11575cfb7f40
5
5
  SHA512:
6
- metadata.gz: f42c5267c1aba6a11a8d6e2b5b7d8c813a1690e3479544294847b4c91474e30ae65ec8407685e7bd0fa7c679c146d6cedb5787d5bbc28fefb71b28f2b5341926
7
- data.tar.gz: edd630a22f94aa3004f3287b031a40bf3ada2de15e5db653c488016941d868cc44dd9f0e983eba2b5e1a8db96da9b1e8a512dd6e59c84c5e0c7e5d8d91bc3060
6
+ metadata.gz: e22610e5725858e4c8a5af4e9868e1fc5ed7dc03c229a393239e153c76bf62b122bbaa3273435530892d38b01ed6e6feeed82e9275f7b24f4f2f993ad027be8f
7
+ data.tar.gz: 4a976b43dfdd6ebd0105bbcce1742897256b01bc3aaa457c7ede59db7b66325039e0c7a8e2c6d52b2a0c7a7bf7853bd26ebf8d087eae9147202e816279e38015
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - "2.1.1"
6
+ script: bundle exec rspec spec
7
+ addons:
8
+ code_climate:
9
+ repo_token:
10
+ secure: f16d367e3357b0375c13e49094a99c656921d3dc0e24a8aa6899e7ad330f857e
11
+
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ [![Build Status](https://travis-ci.org/ssmiech/rubotium.svg?branch=master)](https://travis-ci.org/ssmiech/rubotium)
2
+ [![Code Climate](https://codeclimate.com/github/ssmiech/rubotium/badges/gpa.svg)](https://codeclimate.com/github/ssmiech/rubotium)
3
+ [![Coverage Status](https://coveralls.io/repos/ssmiech/rubotium/badge.svg?branch=master)](https://coveralls.io/r/ssmiech/rubotium?branch=master)
4
+
1
5
  # Rubotium
2
6
 
3
7
  This is an Android's Instrumentation test runner. It's in quite early phase but already solves couple of issues with instrumentation tests:
data/bin/rubotium CHANGED
@@ -10,19 +10,25 @@ opts = Trollop::options do
10
10
  opt :rerun, 'Retries count', :default => 0, :short => '-R'
11
11
  opt :out, 'Report file', :default => 'report.xml', :short => '-o'
12
12
  opt :device, 'Match devices', :type => :string, :short => '-d'
13
+ opt :sdk, 'Run on devices with sdk', :type => :string, :short => '-k'
13
14
  opt :runner, 'Test runner', :type => :string, :short => '-r'
14
15
  opt :annotation, 'Run annotated tests', :type => :string, :short => '-n'
16
+ opt :helper_apk_path, 'Path to the helper .apk file', :type => :string, :short => '-H'
17
+ opt :serial, 'Device serial', :type => :string, :short => '-s'
15
18
  end
16
19
 
17
20
  params = {
21
+ :serial => opts[:serial],
18
22
  :tests_jar_path => opts[:test_jar_path],
19
23
  :tests_apk_path => opts[:test_apk_path],
20
24
  :app_apk_path => opts[:app_apk_path],
21
25
  :rerun_count => opts[:rerun],
22
26
  :report => opts[:out],
23
27
  :device_matcher => opts[:device],
28
+ :device_sdk => opts[:sdk],
24
29
  :runner => opts[:runner],
25
- :annotation => opts[:annotation]
30
+ :annotation => opts[:annotation],
31
+ :helper_apk_path => opts[:helper_apk_path]
26
32
  }
27
33
 
28
34
  Rubotium.new(params)
@@ -6,6 +6,14 @@ module Rubotium
6
6
  @device_serial = device_serial
7
7
  end
8
8
 
9
+ def clean_logcat
10
+ execute(logcat_command(:clean => true))
11
+ end
12
+
13
+ def logcat
14
+ execute(logcat_command(:dump => true))
15
+ end
16
+
9
17
  def install(apk_path)
10
18
  execute(install_command(apk_path))
11
19
  end
@@ -18,20 +26,36 @@ module Rubotium
18
26
  execute(pull_command(files_glob))
19
27
  end
20
28
 
29
+ def push(local_glob, remote_dest)
30
+ execute(push_command(local_glob, remote_dest))
31
+ end
32
+
21
33
  def shell(command)
22
34
  execute(shell_command(command))
23
35
  end
24
36
 
25
37
  def execute(command_to_run)
26
- puts "EXECUTING_COMMAND: #{adb_command} #{command_to_run.executable_command}"
27
- CMD.run_command(adb_command + " " + command_to_run.executable_command)
38
+ commands = command_to_run.executable_command
39
+ puts "EXECUTING_COMMAND: #{adb_command} #{commands}"
40
+
41
+ begin
42
+ commands.each do |command|
43
+ CMD.run_command(adb_command + ' ' + command)
44
+ end
45
+ rescue NoMethodError
46
+ CMD.run_command(adb_command + ' ' + commands)
47
+ end
28
48
  end
29
49
 
30
50
  private
31
51
  attr_reader :device_serial
32
52
 
33
53
  def adb_command
34
- "adb -s #{device_serial} "
54
+ if device_serial.nil? || device_serial == ''
55
+ 'adb'
56
+ else
57
+ "adb -s #{device_serial}"
58
+ end
35
59
  end
36
60
 
37
61
  def install_command(apk_path)
@@ -46,9 +70,17 @@ module Rubotium
46
70
  Rubotium::Adb::Commands::PullCommand.new(files_glob)
47
71
  end
48
72
 
73
+ def push_command(local_glob, remote_dest)
74
+ Rubotium::Adb::Commands::PushCommand.new(local_glob, remote_dest)
75
+ end
76
+
49
77
  def shell_command(command)
50
78
  Rubotium::Adb::Commands::ShellCommand.new(command)
51
79
  end
80
+
81
+ def logcat_command(options = {})
82
+ Rubotium::Adb::Commands::LogcatCommand.new(options)
83
+ end
52
84
  end
53
85
  end
54
86
  end
@@ -0,0 +1,24 @@
1
+ module Rubotium
2
+ module Adb
3
+ module Commands
4
+ class LogcatCommand
5
+ COMMAND = 'logcat'
6
+ def initialize(options)
7
+ @clean = options.delete(:clean) ? '-c' : ''
8
+ @dump = options.delete(:dump) ? '-d' : ''
9
+ end
10
+
11
+ def executable_command
12
+ "#{COMMAND} #{options}"
13
+ end
14
+
15
+ private
16
+ attr_reader :clean, :dump
17
+
18
+ def options
19
+ [clean, dump].join(' ')
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Rubotium
2
+ module Adb
3
+ module Commands
4
+ class PushCommand
5
+ COMMAND = 'push'
6
+
7
+ def initialize(local_glob, remote_dest)
8
+ @local_paths = Dir[local_glob]
9
+ @remote_dest = remote_dest
10
+ end
11
+
12
+ def executable_command
13
+ local_paths.map do |path|
14
+ "#{COMMAND} #{path} #{remote_dest}"
15
+ end
16
+ end
17
+
18
+ private
19
+ attr_reader :local_paths
20
+ attr_reader :remote_dest
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,5 +3,7 @@ require_relative 'commands/install_command'
3
3
  require_relative 'commands/instrument_command'
4
4
  require_relative 'commands/memory_command'
5
5
  require_relative 'commands/pull_command'
6
+ require_relative 'commands/push_command'
6
7
  require_relative 'commands/shell_command'
7
8
  require_relative 'commands/uninstall_command'
9
+ require_relative 'commands/logcat_command'
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  module Rubotium
2
4
  module Adb
3
5
  module Parsers
@@ -31,7 +33,7 @@ module Rubotium
31
33
  end
32
34
 
33
35
  def time
34
- match_time.strip
36
+ match_time.strip.to_f
35
37
  end
36
38
 
37
39
  def message
@@ -0,0 +1,9 @@
1
+ module Rubotium
2
+ module Apk
3
+ class Aapt
4
+ def dump(path)
5
+ CMD.run_command("aapt dump badging \"" + path + "\" 2>&1")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -5,13 +5,18 @@ module Rubotium
5
5
  require "tmpdir"
6
6
  require "pp"
7
7
  class AndroidApk
8
+ attr_reader :aapt
8
9
  attr_accessor :results,:label,:labels,:icon,:icons,:package_name,:version_code,:version_name,:sdk_version,:target_sdk_version,:filepath
9
- def initialize(path_to_apk)
10
+ def initialize(aapt = Aapt.new, path_to_apk)
11
+ @aapt = aapt
10
12
  @path = path_to_apk
11
13
  raise(Errno::ENOENT, "File does not exist") unless File.exist?(@path)
12
14
  end
13
15
 
14
16
  def package_name
17
+ if parsed_aapt['package'].nil?
18
+ raise RuntimeError.new('ERROR: dump failed because no AndroidManifest.xml found')
19
+ end
15
20
  parsed_aapt['package']['name']
16
21
  end
17
22
 
@@ -20,12 +25,11 @@ module Rubotium
20
25
  end
21
26
 
22
27
  def results
23
- command = "aapt dump badging \"" + @path + "\" 2>&1"
24
- results = `#{command}`
25
- if $?.exitstatus != 0 or results.index("ERROR: dump failed")
26
- raise(RuntimeError, results)
28
+ @results ||= aapt.dump(@path)
29
+ if $?.exitstatus != 0 or @results.index("ERROR: dump failed")
30
+ raise(RuntimeError, @results)
27
31
  end
28
- @results ||= results
32
+ @results
29
33
  end
30
34
 
31
35
  def parsed_aapt
data/lib/rubotium/apk.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'apk/android_apk'
2
+ require_relative 'apk/aapt'
2
3
 
3
4
  module Rubotium
4
5
  module Apk
@@ -1,6 +1,6 @@
1
1
  module Rubotium
2
2
  class Device
3
- attr_reader :serial
3
+ attr_reader :serial, :sdk
4
4
  def initialize(serial)
5
5
  @serial = serial
6
6
  end
@@ -9,6 +9,18 @@ module Rubotium
9
9
  @name ||= adb_command.shell('getprop ro.product.model').strip
10
10
  end
11
11
 
12
+ def sdk
13
+ @sdk ||= adb_command.shell('getprop ro.build.version.sdk').strip
14
+ end
15
+
16
+ def clean_logcat
17
+ adb_command.clean_logcat
18
+ end
19
+
20
+ def logcat
21
+ adb_command.logcat
22
+ end
23
+
12
24
  def install(apk_path)
13
25
  adb_command.install(apk_path)
14
26
  end
@@ -3,16 +3,22 @@ module Rubotium
3
3
  def initialize(options = {})
4
4
  @match_serial = options[:serial] || ''
5
5
  @match_name = options[:name] || ''
6
+ @match_sdk = options[:sdk] || ''
6
7
  end
7
8
 
8
9
  def all
9
10
  raise NoDevicesError if attached_devices.empty?
10
- raise NoMatchedDevicesError if matched_by_name.empty?
11
- (matched_by_name + matched_by_serial).uniq
11
+ raise NoMatchedDevicesError if matched_devices.nil? or matched_devices.empty?
12
+ matched_devices
12
13
  end
13
14
 
15
+
14
16
  private
15
- attr_reader :matched, :attached_devices, :match_name, :match_serial
17
+ attr_reader :matched, :attached_devices, :match_name, :match_serial, :match_sdk
18
+
19
+ def matched_devices
20
+ @matched_devices ||=[matched_by_name, matched_by_serial, matched_by_sdk].reject( &:empty? ).reduce( :& )
21
+ end
16
22
 
17
23
  def attached_devices
18
24
  @attached_devices ||= adb_devices.attached
@@ -30,6 +36,12 @@ module Rubotium
30
36
  }
31
37
  end
32
38
 
39
+ def matched_by_sdk
40
+ attached_devices.select { |device|
41
+ device.sdk.eql? match_sdk
42
+ }
43
+ end
44
+
33
45
  def adb_devices
34
46
  Adb::Devices.new
35
47
  end
@@ -0,0 +1,97 @@
1
+ body {
2
+ margin: 0 auto;
3
+ padding: 0;
4
+ text-align: left;
5
+ height: 100%;
6
+ font-family: myriad, arial, tahoma, verdana, sans-serif;
7
+ color: #151515;
8
+ font-size: 90%;
9
+ line-height: 1.3em;
10
+ background-color: #fff;
11
+ }
12
+
13
+ ul {
14
+ list-style: none;
15
+ }
16
+
17
+ #header {
18
+ padding: 0;
19
+ position: fixed;
20
+ width: 100%;
21
+ z-index: 10;
22
+ background-color: #c7ceda;
23
+ }
24
+
25
+ #header .time {
26
+ margin-top: 2.2em;
27
+ margin-right: 3.4em;
28
+ float: right;
29
+ }
30
+
31
+ #header h1 {
32
+ margin: 1em 3em 1em 1.7em;
33
+ color: #151515;
34
+ font-size: 180%;
35
+ line-height: 1.1em;
36
+ font-weight: bold;
37
+ }
38
+ .testsuite {
39
+ margin: 5px;
40
+ }
41
+
42
+ .testsuite > div {
43
+ padding: .5em 0 .5em 1em;
44
+ background-color: #f2f2f2;
45
+ font-size: 120%;
46
+ color: #151515;
47
+ font-weight: bold;
48
+ }
49
+
50
+ span.error {
51
+ color: #ff0000;
52
+ }
53
+
54
+ span.passed {
55
+ color: #1d9d01;
56
+ }
57
+
58
+ #content {
59
+ padding: 112px 2.5em 2em 1.7em;
60
+ }
61
+
62
+ .test_case > div{
63
+ padding: .5em 0 .5em 1em;
64
+ color: #0046b0;
65
+ font-size: 100%;
66
+ font-weight: bold;
67
+ border-bottom: solid 1px #dbdbdb;
68
+ }
69
+
70
+ .test_case .time {
71
+ margin-right: .5em;
72
+ width: 5em;
73
+ text-align: right;
74
+ font-size: 13px;
75
+ color: #151515;
76
+ font-style: normal;
77
+ font-weight: normal;
78
+ float: right;
79
+ }
80
+
81
+ #testsuites .passed {
82
+ border-left: solid 10px #93e078;
83
+ }
84
+
85
+ #testsuites .failed {
86
+ border-left: solid 10px #ff0000;
87
+ }
88
+
89
+ .stderr {
90
+ color: #8b0000 !important;
91
+ font-size: 80% !important;
92
+ font-weight: normal !important;
93
+ overflow-x: auto;
94
+ cursor: auto !important;
95
+ background: none !important;
96
+ border: none !important;
97
+ }
@@ -0,0 +1,181 @@
1
+ require 'builder'
2
+ require 'uri'
3
+
4
+ module Rubotium
5
+ module Formatters
6
+ class HtmlFormatter
7
+ attr_reader :html
8
+ def initialize(results, path_to_file)
9
+ @test_results = results
10
+ @report_file_path = path_to_file
11
+
12
+ @html = Builder::XmlMarkup.new :target => ensure_io(report_path), :indent => 2
13
+ inline_css
14
+ add_header
15
+ add_body
16
+ add_footer
17
+ end
18
+
19
+ private
20
+ attr_reader :report_file_path, :device_serial, :test_results
21
+
22
+ def inline_css
23
+ # html.link(:rel=>"stylesheet", :href=> (File.dirname(__FILE__) + '/html_formatter.css'))
24
+ html.style(:type => 'text/css') do
25
+ html << File.read(File.dirname(__FILE__) + '/html_formatter.css')
26
+ end
27
+ end
28
+
29
+ def add_header
30
+ html.div(:id => 'header') do
31
+ display_time
32
+ display_info
33
+ end
34
+ end
35
+
36
+ def display_info
37
+ html.h1 do
38
+ html.text('All tests:')
39
+ html.strong do
40
+ html.span("#{test_results.total} total,")
41
+ html.span("#{test_results.passed} passed,", { :class => 'passed' })
42
+ html.span("#{test_results.failed} failed", { :class => 'error' }) if test_results.failed
43
+
44
+ end
45
+ end
46
+ end
47
+
48
+ def display_time
49
+ html.div(format_time(test_results.time.to_i), { :class => 'time' })
50
+ end
51
+
52
+ def add_body
53
+ html.body do
54
+ add_content
55
+ end
56
+ end
57
+
58
+ def add_footer
59
+
60
+ end
61
+
62
+ def add_content
63
+ html.div(:id => 'content') do
64
+ html.ul(:id => 'testsuites') do
65
+ test_results.each{|suite|
66
+ start_test_suite(suite)
67
+ }
68
+ end
69
+ end
70
+ end
71
+
72
+ def start_test_suite(suite)
73
+ html.li(:class => 'testsuite') do
74
+ html.div(:class => suite.state) do
75
+ html.span(suite.name)
76
+ end
77
+
78
+ html.ul do
79
+ suite.each_test { |test|
80
+ html << TestCaseHtml.new(test).target
81
+ }
82
+ end
83
+
84
+ end
85
+ end
86
+
87
+ def report_path
88
+ report_file_path
89
+ end
90
+
91
+ def ensure_io(path_to_file)
92
+ File.open(path_to_file, 'w')
93
+ end
94
+
95
+ def format_time(seconds)
96
+ return '0 s' if seconds == 0
97
+ [[60, :s], [60, :m], [24, :h]].map{ |count, name|
98
+ if seconds > 0
99
+ seconds, n = seconds.divmod(count)
100
+ "#{n.to_i} #{name}"
101
+ end
102
+ }.compact.reverse.join(' ')
103
+ end
104
+
105
+ class TestCaseHtml
106
+ def initialize(test_result)
107
+ @test_result = test_result
108
+ @builder = Builder::XmlMarkup.new(:indent => 2)
109
+ end
110
+
111
+ def target
112
+ html
113
+ end
114
+
115
+ private
116
+ attr_reader :test_result, :builder
117
+
118
+ def html
119
+ builder.li(:class => 'test_case') do
120
+ print_name
121
+ builder.ul do
122
+ unless test_result.passed?
123
+ print_stacktrace
124
+ print_logcat
125
+ print_screen_record if File.exist?(File.join(Dir.pwd, 'results', video_file))
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def print_stacktrace
132
+ builder.li(:class => 'stderr') do
133
+ test_result.stack_trace.each_line{|line|
134
+ builder.br
135
+ builder.span(line)
136
+ }
137
+ end
138
+ end
139
+
140
+ def print_logcat
141
+ builder.li(:class => 'logcat') do
142
+ builder.a("full log", :href => URI.encode(logcat_file))
143
+ end
144
+ end
145
+
146
+ def print_screen_record
147
+ builder.li(:class => 'screencast') do
148
+ builder.video({ :width => "240", :height => "320", :controls => true }) do
149
+ builder.source({ :src => URI.encode(video_file), :type => "video/mp4" })
150
+ end
151
+ end
152
+ end
153
+
154
+ def print_name
155
+ builder.div(:class => test_result.passed? ? 'passed' : 'failed') do
156
+ builder.div(format_time(test_result.time), { :class => 'time' })
157
+ builder.span(test_result.test_name)
158
+ end
159
+ end
160
+
161
+ def logcat_file
162
+ "logs/#{test_result.name}.log"
163
+ end
164
+
165
+ def video_file
166
+ "screencasts/#{test_result.name}.mp4"
167
+ end
168
+
169
+ def format_time(seconds)
170
+ return '0 s' if seconds == 0
171
+ [[60, :s], [60, :m], [24, :h]].map{ |count, name|
172
+ if seconds > 0
173
+ seconds, n = seconds.divmod(count)
174
+ "#{n.to_i} #{name}"
175
+ end
176
+ }.compact.reverse.join(' ')
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,43 @@
1
+ module Rubotium
2
+ class Recorder
3
+ SIGINT = 'SIGINT'
4
+
5
+ def initialize(device_serial)
6
+ @serial = device_serial
7
+ end
8
+
9
+ def start(test_name)
10
+ @pid = Process.spawn(record_command(test_name))
11
+ puts "Starting screencast, PID: #{@pid}"
12
+ end
13
+
14
+ def record_command(test_name)
15
+ "adb -s #{serial} shell screenrecord /sdcard/screencasts/#{test_name}.mp4"
16
+ end
17
+
18
+ def stop
19
+ puts "Stopping screencast, PID: #{@pid}"
20
+ kill_children
21
+ kill_self
22
+ end
23
+
24
+ private
25
+ attr_reader :serial
26
+
27
+ def kill_self
28
+ Process.kill(SIGINT, @pid)
29
+ end
30
+
31
+ def kill_children
32
+ find_child_processes.each{|pid|
33
+ if pid > 0
34
+ Process.kill(SIGINT, pid)
35
+ end
36
+ }
37
+ end
38
+
39
+ def find_child_processes
40
+ `pgrep -P #{@pid}`.each_line.map(&:strip).map(&:to_i)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1 @@
1
+ require_relative 'screencast/recorder'
@@ -9,6 +9,10 @@ module Rubotium
9
9
  @test_name = runnable_test.test_name
10
10
  end
11
11
 
12
+ def name
13
+ "#{class_name}##{test_name}"
14
+ end
15
+
12
16
  def stack_trace
13
17
  if(failed_run?)
14
18
  parsed_result.message