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.
- checksums.yaml +7 -7
- data/.gitignore +4 -1
- data/.rubocop.yml +865 -0
- data/.travis.yml +11 -0
- data/README.md +30 -11
- data/Rakefile +6 -5
- data/bin/monkey_master +29 -30
- data/lib/monkey_master.rb +1 -1
- data/lib/monkey_master/adb.rb +81 -0
- data/lib/monkey_master/monkey_commander.rb +125 -149
- data/lib/monkey_master/version.rb +1 -1
- data/monkey_master.gemspec +6 -3
- data/spec/monkey_master_spec.rb +106 -0
- data/spec/spec_helper.rb +7 -0
- metadata +97 -44
- data/test/test_monkey_master.rb +0 -50
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
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
|
-
|
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
|
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
|
-
|
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,
|
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
|
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
|
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
|
-
|
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
|
-
|
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 '
|
2
|
+
require 'rspec/core/rake_task'
|
3
3
|
|
4
|
-
|
5
|
-
|
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
|
-
|
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
|
19
|
-
--version
|
20
|
-
--iterations <iterations>
|
21
|
-
|
22
|
-
--devices <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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
46
|
+
puts e.message
|
48
47
|
rescue ArgumentError => e
|
49
|
-
|
48
|
+
puts "ERROR: Invalid arguments: #{e.message}"
|
50
49
|
end
|
51
50
|
|
52
|
-
exit 1
|
51
|
+
exit 1
|
data/lib/monkey_master.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|