monkey_master 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+
6
+ notifications:
7
+ email:
8
+ recipients:
9
+ - lukas.nagl@innovaptor.com
10
+ on_failure: change
11
+ on_success: never
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
- *monkey_master* - A tool for conveniently employing Android adb monkeys.
1
+ `monkey_master` - A tool for conveniently employing Android adb monkeys.
2
2
  ================================================================
3
+
4
+ [![Build Status](https://travis-ci.org/j4zz/monkey_master.svg)](https://travis-ci.org/j4zz/monkey_master)
5
+
3
6
  Android's adb offers the ui/application exerciser [monkey](http://developer.android.com/tools/help/monkey.html). Conveniently employing it can be cumbersome, though:
4
7
 
5
8
  * It's inconvenient to kill a running monkey.
@@ -7,15 +10,15 @@ Android's adb offers the ui/application exerciser [monkey](http://developer.andr
7
10
  * You either watch the log in your (running) sdk, or you manually handle logcat.
8
11
  * Managing all of the above on multiple devices is a real pain.
9
12
 
10
- *monkey_master* is a convenience tool for solving these issues. It can easily be combined with other tools, for example to build a fully automated build & test system.
13
+ `monkey_master` is a convenience tool for solving these issues. It can easily be combined with other tools, for example to build a fully automated build & test system.
11
14
 
12
15
  Besides having convenience commands for starting and killing adb monkeys, it has multi-device support (simultaneously running monkeys on multiple devices) and automatically creates log files for each device.
13
16
 
14
- For an example of a *monkey_master* test setup, and the reasoning behind the project, visit: [http://innovaptor.com/blog/2013/08/18/building-an-automated-testing-and-error-reporting-system-for-android-apps-with-monkey-master-and-crashlytics.html](http://innovaptor.com/blog/2013/08/18/building-an-automated-testing-and-error-reporting-system-for-android-apps-with-monkey-master-and-crashlytics.html)
17
+ For an example of a `monkey_master` test setup, and the reasoning behind the project, visit [this blog post](http://innovaptor.com/blog/2013/08/18/building-an-automated-testing-and-error-reporting-system-for-android-apps-with-monkey-master-and-crashlytics.html).
15
18
 
16
19
  Installation
17
20
  ================================================================
18
- *monkey_master* is available as a ruby gem:
21
+ `monkey_master` is available as a ruby gem:
19
22
 
20
23
  gem install monkey_master
21
24
 
@@ -23,7 +26,7 @@ Installation
23
26
 
24
27
  export PATH=/YOUR/PATH/android-sdks/platform-tools:$PATH
25
28
 
26
- Furthermore, you need to have a device in development mode connected. Currently, *monkey_master* is not tested with an emulator. For a list of connected devices, use `adb devices`.
29
+ Furthermore, you need to have a device in *development mode* connected. Currently, `monkey_master` is not tested with an emulator. For a list of connected devices, use `adb devices`.
27
30
 
28
31
  Usage
29
32
  ================================================================
@@ -40,18 +43,34 @@ An example for a test run could be:
40
43
 
41
44
  monkey_master com.my.App --iterations 100
42
45
 
43
- If you want to stop the monkeys, either SIGINT the *monkey_master* during execution,
44
- or call:
46
+ If you want to stop the monkeys, either SIGINT (keyboard interrupt) the `monkey_master` during execution, or call:
45
47
 
46
48
  monkey_master -k
47
49
 
48
- If you have multiple devices connected, and want to use *monkey_master* on some of them only,
49
- call:
50
+ If you have multiple devices connected, and want to use `monkey_master` on some of them only, call:
50
51
 
51
52
  monkey_master com.my.App --devices DEVICEID1,DEVICEID2 --iterations 100
52
53
 
53
- Contributing
54
+ Crash Reporting
55
+ ================================================================
56
+ You might want to use `monkey_master` with a crash reporting tool such as [fabric](https://fabric.io/). Simply integrate the crash reporting library of your choice in your Android App, and `monkey_master`’s crashes will get reported there.
57
+
58
+ Android Edge Cases
54
59
  ================================================================
55
- The initial version of *monkey_master* has been created with little ruby knowledge and with the pressure of an immediate need.
60
+ Sometimes you encounter errors that only a monkey can generate, but that you can’t reasonably fix in your code. In case of such an error, you will want to exit gracefully if the user is a monkey (in order to suppress useless crash reports), but leave the error handling as it is for production.
56
61
 
62
+ Furthermore, you might want to disable certain network calls, or redirect them to a test server.
63
+
64
+ For such cases, there’s ActivityManager.isUserAMonkey():
65
+
66
+ ```java
67
+ if(ActivityManager.isUserAMonkey()) {
68
+ // Work on the test server
69
+ } else {
70
+ // Work on the production server
71
+ }
72
+ ```
73
+
74
+ Contributing
75
+ ================================================================
57
76
  Code style or beauty fixes are just as welcome as pull requests, bug reports or ideas.
data/Rakefile CHANGED
@@ -1,9 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
- require 'rake/testtask'
2
+ require 'rspec/core/rake_task'
3
3
 
4
- Rake::TestTask.new do |t|
5
- t.libs << 'test'
4
+ # Default directory to look in is `/specs`
5
+ # Run with `rake spec`
6
+ RSpec::Core::RakeTask.new(:spec) do |task|
7
+ task.rspec_opts = ['--color', '--format', 'documentation']
6
8
  end
7
9
 
8
- desc "Run tests"
9
- task :default => :test
10
+ task default: :spec
data/bin/monkey_master CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'docopt'
4
-
5
4
  require 'monkey_master'
6
5
 
7
6
  # Parse Command Line Options
@@ -9,44 +8,44 @@ doc = <<DOCOPT
9
8
  A tool for conveniently employing Android adb monkeys.
10
9
 
11
10
  Usage:
12
- #{__FILE__} <app_id> [--devices <devices>] [--iterations <iterations>] [-k]
11
+ #{__FILE__} <app_id> [--devices <devices>] [--iterations <iterations>] [-k] [--adb <adb_args>]
13
12
  #{__FILE__} -k
14
13
  #{__FILE__} -h | --help
15
14
  #{__FILE__} --version
16
15
 
17
16
  Options:
18
- -h --help Show this screen.
19
- --version Show version.
20
- --iterations <iterations> The number of monkeys that should be run consecutively.
21
- It is preferable to run a high number of iterations of short-lived monkeys in order to handle freezes better.
22
- --devices <devices> Devices which should be used by the monkey commander separated by a ','. If not given, uses all devices.
23
-
17
+ -h --help Show this screen.
18
+ --version Show version.
19
+ --iterations <iterations> The number of monkeys that should be run consecutively.
20
+ It is preferable to run a high number of iterations of short-lived monkeys in order to handle freezes better.
21
+ --devices <devices> Devices which should be used by the monkey commander separated by a ','. If not given, uses all devices.
22
+ --adb <adb_args> Arguments for running the adb monkey as a string, e.g. "--throttle 500". If not provided, reasonable defaults will be used.
24
23
  DOCOPT
25
24
 
26
25
  begin
27
- opts = Docopt::docopt(doc)
28
- commander = MonkeyMaster::MonkeyCommander.new(opts["<app_id>"])
29
-
30
- devices = opts["--devices"]
31
- commander.detect_devices(devices)
32
-
33
- if(opts["-k"])
34
- commander.kill_monkeys
35
- end
36
-
37
- if(opts["<app_id>"])
38
- # An app id has been given, proceed with starting monkeys on the devices
39
- iterations = opts["--iterations"]
40
- if(iterations)
41
- commander.iterations = iterations
42
- end
43
- commander.command_monkeys
44
- end
45
- exit 0
26
+ opts = Docopt::docopt(doc)
27
+ unless MonkeyMaster::ADB.adb?
28
+ puts 'ERROR: adb is not installed or not accessible'
29
+ exit 2
30
+ end
31
+ commander = MonkeyMaster::MonkeyCommander.new(opts['<app_id>'])
32
+
33
+ devices = opts['--devices']
34
+ commander.detect_devices(devices)
35
+
36
+ commander.kill_monkeys if opts['-k']
37
+
38
+ if opts['<app_id>']
39
+ # An app id has been given, proceed with starting monkeys on the devices
40
+ iterations = opts['--iterations']
41
+ commander.iterations = iterations if iterations
42
+ commander.command_monkeys(opts['--adb'])
43
+ end
44
+ exit 0
46
45
  rescue Docopt::Exit => e
47
- puts e.message
46
+ puts e.message
48
47
  rescue ArgumentError => e
49
- puts "ERROR: Invalid arguments: " + e.message
48
+ puts "ERROR: Invalid arguments: #{e.message}"
50
49
  end
51
50
 
52
- exit 1
51
+ exit 1
data/lib/monkey_master.rb CHANGED
@@ -2,4 +2,4 @@ module MonkeyMaster
2
2
  end
3
3
 
4
4
  require 'monkey_master/version'
5
- require 'monkey_master/monkey_commander.rb'
5
+ require 'monkey_master/monkey_commander'
@@ -0,0 +1,81 @@
1
+ require 'mkmf'
2
+ require 'pry'
3
+
4
+ module MonkeyMaster
5
+ # Provide helpers to work with Android ADB
6
+ class ADB
7
+ # Run the adb monkey.
8
+ #
9
+ # +app_id+:: ID of the android app for which the monkey should be run
10
+ # +device+:: Device on which the adb monkey should be run
11
+ # +args+:: Arguments passed to the adb monkey
12
+ def self.monkey_run(app_id, device, args)
13
+ `adb -s #{device} shell monkey -p #{app_id} #{args}`
14
+ $?.exitstatus
15
+ end
16
+
17
+ # Force stop a monkey for an app.
18
+ #
19
+ # +app_id+:: ID of the android app for which the monkey should be stopped
20
+ def self.monkey_stop(app_id, device)
21
+ `adb -s #{device} shell am force-stop #{app_id}`
22
+ end
23
+
24
+ # Use ADB to detect connected Android devices.
25
+ def self.detect_devices
26
+ device_list = `adb devices | grep -v "List" | grep "device" | awk '{print $1}'`
27
+ device_list.split("\n")
28
+ end
29
+
30
+ # Kill ADB monkeys.
31
+ #
32
+ # +device+:: Devices for which the adb monkey should be killed
33
+ def self.kill_monkeys(devices)
34
+ unless devices
35
+ puts '[ADB] No devices specified yet.'
36
+ return
37
+ end
38
+
39
+ devices.each do |device|
40
+ puts "[ADB] KILLING the monkey on device #{device}."
41
+ `adb -s #{device} shell ps | awk '/com\.android\.commands\.monkey/ { system("adb -s #{device} shell kill " $2) }'`
42
+ end
43
+ end
44
+
45
+ # Start logging on a certain device with logcat
46
+ #
47
+ # +app_id+:: App for which logs should be retrieved
48
+ # +device+:: Device for which logging should be started
49
+ # +log+:: File that should be used for logging
50
+ def self.start_logging(app_id, device, log)
51
+ begin
52
+ timeout(5) do
53
+ puts "[ADB/LOGS] Logging device #{device} to #{log}."
54
+ `adb -s #{device} logcat -c #{log} &`
55
+ `adb -s #{device} logcat #{app_id}:W > #{log} &`
56
+ end
57
+ rescue Timeout::Error
58
+ end_logging
59
+ raise ArgumentError, 'It doesn’t seem like there are ready, connected devices.'
60
+ end
61
+ end
62
+
63
+ # End logging on multiple devices.
64
+ #
65
+ # +device+:: Devices for which logging should be stopped
66
+ def self.end_logging(devices)
67
+ devices.each do |device|
68
+ puts "[ADB/LOGS] KILLING the logcat process on device #{device}."
69
+ `adb -s #{device} shell ps | grep -m1 logcat | awk '{print $2}' | xargs adb -s #{device} shell kill`
70
+ puts "[ADB/LOGS] KILLING the logcat process for the device #{device} on the machine."
71
+ `ps ax | grep -m1 "adb -s #{device} logcat" | awk '{print $1}' | xargs kill`
72
+ end
73
+ end
74
+
75
+ # Check if adb is accessible as an executable
76
+ def self.adb?
77
+ adb = find_executable 'adb'
78
+ adb.nil? ? false : true
79
+ end
80
+ end
81
+ end
@@ -1,155 +1,131 @@
1
1
  require 'fileutils'
2
2
  require 'logger'
3
3
  require 'timeout'
4
+ require 'pry'
5
+ require_relative 'adb'
4
6
 
5
7
  module MonkeyMaster
6
- # A class for conveniently employing Android adb monkeys.
7
- #
8
- # Author:: Lukas Nagl (mailto:lukas.nagl@innovaptor.com)
9
- # Copyright:: Copyright (c) 2013 Innovaptor OG
10
- # License:: MIT
11
- class MonkeyCommander
12
- # Directory of the monkey logs.
13
- attr_reader :log_dir
14
- # Logger used for the monkey output.
15
- attr_reader :logger
16
- # The id of the app that should be tested by monkeys. E.g.: com.innovaptor.MonkeyTestApp
17
- attr_writer :app_id
18
- # The number of monkey iterations that should be run on each device.
19
- attr_writer :iterations
20
- # List of devices that should be used by the MonkeyCommander.
21
- attr_writer :device_list
22
- public
23
- # Initialize the monkey master.
24
- #
25
- # +app_id+:: The id of the app that should be tested by the monkeys, e.g. com.innovaptor.MonkeyTestApp
26
- def initialize(app_id)
27
- @app_id = app_id
28
- @iterations = 1 # Default to a single iteration
29
- @base_dir = Dir.pwd
30
- time = Time.new
31
- @log_dir = "monkey_logs" + time.strftime("%Y%m%d_%H%M%S")
32
- @logger = Logger.new(STDOUT)
33
- @logger.formatter = proc { |severity, datetime, progname, msg|
34
- "#{severity}|#{datetime}: #{msg}\n"
35
- }
36
- end
37
-
38
- # Either create a list of devices from the parameter,
39
- # or detect connected devices using adb.
40
- #
41
- # +devices+:: nil, for automatic device detection; or a list of device IDs separated by ','
42
- def detect_devices(devices)
43
- if(devices)
44
- # Devices are given, create a list
45
- devices = devices.split(',')
46
- @device_list = devices
47
- else
48
- # No devices specified, detect them
49
- device_list = %x(adb devices | grep -v "List" | grep "device" | awk '{print $1}')
50
- device_list = device_list.split("\n")
51
- @device_list = device_list
52
- end
53
- end
54
-
55
- # Kill the monkey on each device.
56
- def kill_monkeys
57
- if(@device_list)
58
- @device_list.each{|device|
59
- @logger.info("[CLEANUP] KILLING the monkey on device #{device}.")
60
- %x(adb -s #{device} shell ps | awk '/com\.android\.commands\.monkey/ { system("adb -s #{device} shell kill " $2) }')
61
- }
62
- else
63
- @logger.warn("[CLEANUP] No devices specified yet.")
64
- end
65
- end
66
-
67
- # Start running monkeys on all specified devices.
68
- def command_monkeys
69
- if(!@device_list || @device_list.empty?)
70
- raise ArgumentError, "No devices found or specified."
71
- end
72
- if(!@app_id)
73
- raise ArgumentError, "No app id specified."
74
- end
75
- prepare
76
-
77
- masters = []
78
- begin
79
- @device_list.each{|device|
80
- master = Thread.new{
81
- # Monkey around in parallel
82
-
83
- log_device_name = "monkey_current" + device + ".txt";
84
- current_log = File.join(@log_dir, log_device_name)
85
- start_logging(device, current_log)
86
- @logger.info("[MASTER #{device}] Starting to command monkeys.")
87
- @iterations.to_i.times do |i|
88
- @logger.info("\t[MASTER #{device}] Monkey " + i.to_s + " is doing its thing...")
89
-
90
- # Start the monkey
91
- %x(adb -s #{device} shell monkey -p #{@app_id} -v 80000 --throttle 100 --ignore-timeouts --pct-majornav 10 --pct-appswitch 0 --kill-process-after-error)
92
- if($? != 0)
93
- @logger.info("\t\t[MASTER #{device}] Monkey encountered an error!")
94
- end
95
-
96
- # Archive the log
97
- log_archiving_name = "monkeylog_" + device + "_" + i.to_s + ".txt"
98
- FileUtils.cp(current_log, File.join(@log_dir, log_archiving_name))
99
-
100
- # Clean the current log
101
- File.truncate(current_log, 0)
102
- @logger.info("\t\t[MASTER #{device}] Monkey " + i.to_s + " is killing the app now in preparation for the next monkey.")
103
- %x(adb -s #{device} shell am force-stop #{@app_id})
104
- end
105
- @logger.info("[MASTER #{device}] All monkeys are done.")
106
- }
107
- masters.push(master)
108
- }
109
-
110
- masters.each{|master| master.join} # wait for all masters to finish
111
- rescue SystemExit, Interrupt
112
- # Clean and graceful shutdown, if possible
113
- @logger.info("[MASTER] Received interrupt. Stopping all masters.")
114
- masters.each{|master| master.terminate}
115
- end
116
-
117
- kill_monkeys
118
- end_logging
119
- end
120
-
121
- private
122
- # Do all necessary preparations that are necessary for the monkeys to run.
123
- def prepare
124
- if(!File.directory?(@log_dir))
125
- Dir.mkdir(@log_dir);
126
- @logger.info("[SETUP] Writing to the following folder: #{@log_dir}")
127
- end
128
- kill_monkeys
129
- end
130
-
131
- # Start logging on all devices.
132
- def start_logging(device, current_log)
133
- begin
134
- Timeout::timeout(5) {
135
- @logger.info("[SETUP] Creating the following log file: #{current_log}")
136
- %x(adb -s #{device} logcat -c #{current_log} &)
137
- %x(adb -s #{device} logcat *:W > #{current_log} &)
138
- }
139
- rescue Timeout::Error
140
- end_logging
141
- raise ArgumentError, "It doesn't seem like there are ready, connected devices."
142
- end
143
- end
144
-
145
- # End logging on all devices.
146
- def end_logging
147
- @device_list.each{|device|
148
- @logger.info("[CLEANUP] KILLING the logcat process on device #{device}.")
149
- %x(adb -s #{device} shell ps | grep -m1 logcat | awk '{print $2}' | xargs adb -s #{device} shell kill)
150
- @logger.info("[CLEANUP] KILLING the logcat process for the device #{device} on the machine.")
151
- %x(ps ax | grep -m1 "adb -s #{device} logcat" | awk '{print $1}' | xargs kill)
152
- }
153
- end
154
- end
8
+ # A class for conveniently employing Android adb monkeys.
9
+ #
10
+ # Author:: Lukas Nagl (mailto:lukas.nagl@innovaptor.com)
11
+ # Copyright:: Copyright (c) 2013 Innovaptor OG
12
+ # License:: MIT
13
+ class MonkeyCommander
14
+ # Directory of the monkey logs.
15
+ attr_reader :log_dir
16
+ # Logger used for the monkey output.
17
+ attr_reader :logger
18
+ # The id of the app that should be tested by monkeys. E.g.: com.innovaptor.MonkeyTestApp
19
+ attr_writer :app_id
20
+ # The number of monkey iterations that should be run on each device.
21
+ attr_writer :iterations
22
+ # List of devices that should be used by the MonkeyCommander.
23
+ attr_writer :device_list
24
+
25
+ public
26
+
27
+ # Initialize the monkey master.
28
+ #
29
+ # +app_id+:: The id of the app that should be tested by the monkeys, e.g. com.innovaptor.MonkeyTestApp
30
+ def initialize(app_id)
31
+ @app_id = app_id
32
+ @iterations = 1 # Default to a single iteration
33
+ @base_dir = Dir.pwd
34
+ time = Time.new
35
+ @log_dir = 'monkey_logs' + time.strftime('%Y%m%d_%H%M%S')
36
+ @logger = Logger.new(STDOUT)
37
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
38
+ "#{severity}|#{datetime}: #{msg}\n"
39
+ end
40
+ end
41
+
42
+ # Either create a list of devices from the parameter,
43
+ # or detect connected devices using adb.
44
+ #
45
+ # +devices+:: nil, for automatic device detection; or a list of device IDs separated by ','
46
+ def detect_devices(devices)
47
+ @device_list = devices ? devices.split(',') : ADB.detect_devices
48
+ end
49
+
50
+ # Kill the monkey on all detected devices.
51
+ def kill_monkeys
52
+ ADB.kill_monkeys(@device_list)
53
+ end
54
+
55
+ # Start running monkeys on all specified devices.
56
+ #
57
+ # +adb_args+:: Arguments passed to the adb monkeys
58
+ def command_monkeys(adb_args='-v 80000 --throttle 200 --ignore-timeouts --pct-majornav 20 --pct-appswitch 0 --kill-process-after-error')
59
+ @logger.info("[SETUP] Will run adb monkeys with the following arguments: #{adb_args}")
60
+ if !@device_list || @device_list.empty?
61
+ fail(ArgumentError, 'No devices found or specified. Check if development mode is on.')
62
+ end
63
+
64
+ fail(ArgumentError, 'No app id specified.') unless @app_id
65
+
66
+ prepare
67
+
68
+ masters = []
69
+ begin
70
+ @device_list.each do |device|
71
+ master = Thread.new do
72
+ # Monkey around in parallel
73
+
74
+ device_log = log_for_device(@app_id, device)
75
+
76
+ @logger.info("[MASTER #{device}] Starting to command monkeys.")
77
+ @iterations.to_i.times do |i|
78
+ @logger.info("\t[MASTER #{device}] Monkey #{i} is doing its thing…")
79
+
80
+ # Start the monkey
81
+ if ADB.monkey_run(@app_id, device, adb_args) != 0
82
+ @logger.info("\t\t[MASTER #{device}] Monkey encountered an error!")
83
+ end
84
+
85
+ # Archive and clean the log
86
+ archive_and_clean_log(device_log, "monkeylog_#{device}_#{i}.txt")
87
+
88
+ @logger.info("\t\t[MASTER #{device}] Monkey #{i} is killing the app now in preparation for the next monkey.")
89
+ ADB.monkey_stop(@app_id, device)
90
+ end
91
+ @logger.info("[MASTER #{device}] All monkeys are done.")
92
+ end
93
+ masters.push(master)
94
+ end
95
+
96
+ masters.each(&:join) # wait for all masters to finish
97
+ rescue SystemExit, Interrupt
98
+ # Clean and graceful shutdown, if possible
99
+ @logger.info('[MASTER] Received interrupt. Stopping all masters.')
100
+ masters.each(&:terminate)
101
+ end
102
+
103
+ ADB.kill_monkeys(@device_list)
104
+ ADB.end_logging(@device_list)
105
+ end
106
+
107
+ private
108
+
109
+ def archive_and_clean_log(device_log, name)
110
+ FileUtils.cp(device_log, File.join(@log_dir, name))
111
+ File.truncate(device_log, 0)
112
+ end
113
+
114
+ # start monkey log for a certain device
115
+ def log_for_device(app_id, device)
116
+ log_device_name = "monkey_current#{device}.txt"
117
+ log = File.join(@log_dir, log_device_name)
118
+ ADB.start_logging(app_id, device, log)
119
+ log
120
+ end
121
+
122
+ # Do all necessary preparations that are necessary for the monkeys to run.
123
+ def prepare
124
+ unless File.directory?(@log_dir)
125
+ Dir.mkdir(@log_dir)
126
+ @logger.info("[SETUP] Writing to the following folder: #{@log_dir}")
127
+ end
128
+ ADB.kill_monkeys(@device_list)
129
+ end
130
+ end
155
131
  end